handles git repositories

This commit is contained in:
Alessandro De Maria 2025-07-06 01:23:36 +00:00
parent 375b670ac8
commit ee3e4215f8

View file

@ -10,6 +10,7 @@
//! - Build and resolution configuration //! - Build and resolution configuration
use std::fmt; use std::fmt;
use std::hash::{Hash, Hasher};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uv_platform_tags::PlatformTag; use uv_platform_tags::PlatformTag;
@ -96,14 +97,16 @@ pub struct PexLockedRequirement {
pub struct PexArtifact { pub struct PexArtifact {
/// The artifact URL. /// The artifact URL.
pub url: String, pub url: String,
/// The filename. /// The filename (optional for git dependencies).
pub filename: String, #[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
/// Hash algorithm (e.g., "sha256"). /// Hash algorithm (e.g., "sha256").
pub algorithm: String, pub algorithm: String,
/// Hash value. /// Hash value.
pub hash: String, pub hash: String,
/// Whether this is a wheel. /// Whether this is a wheel (optional for git dependencies).
pub is_wheel: bool, #[serde(skip_serializing_if = "Option::is_none")]
pub is_wheel: Option<bool>,
} }
impl PexLock { impl PexLock {
@ -154,6 +157,56 @@ impl PexLock {
// Create locked requirement // Create locked requirement
let mut artifacts = Vec::new(); 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) // Add wheels (excluding Windows-specific wheels for Linux/Mac targets)
for wheel in &package.wheels { for wheel in &package.wheels {
// Filter out Windows-specific wheels when targeting linux/mac // Filter out Windows-specific wheels when targeting linux/mac
@ -185,10 +238,10 @@ impl PexLock {
artifacts.push(PexArtifact { artifacts.push(PexArtifact {
url: wheel_url, url: wheel_url,
filename: wheel.filename.to_string(), filename: Some(wheel.filename.to_string()),
algorithm, algorithm,
hash, hash,
is_wheel: true, is_wheel: Some(true),
}); });
} }
@ -219,20 +272,36 @@ impl PexLock {
let (algorithm, hash) = if let Some(h) = sdist.hash() { let (algorithm, hash) = if let Some(h) = sdist.hash() {
Self::parse_hash(&h.to_string()) 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 { } else {
continue; continue;
}; };
artifacts.push(PexArtifact { artifacts.push(PexArtifact {
url: sdist_url, url: sdist_url,
filename: sdist_filename, filename: Some(sdist_filename),
algorithm, algorithm,
hash, 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 // Only include packages that have at least one artifact
if !artifacts.is_empty() { if !artifacts.is_empty() {
// Collect dependencies for this package (only those with compatible artifacts) // Collect dependencies for this package (only those with compatible artifacts)
@ -376,7 +445,7 @@ mod tests {
let (algorithm, hash) = PexLock::parse_hash("sha256:abcd1234"); let (algorithm, hash) = PexLock::parse_hash("sha256:abcd1234");
assert_eq!(algorithm, "sha256"); assert_eq!(algorithm, "sha256");
assert_eq!(hash, "abcd1234"); assert_eq!(hash, "abcd1234");
let (algorithm, hash) = PexLock::parse_hash("md5:1234abcd"); let (algorithm, hash) = PexLock::parse_hash("md5:1234abcd");
assert_eq!(algorithm, "md5"); assert_eq!(algorithm, "md5");
assert_eq!(hash, "1234abcd"); assert_eq!(hash, "1234abcd");
@ -393,10 +462,10 @@ mod tests {
fn test_pex_artifact_structure() { fn test_pex_artifact_structure() {
let artifact = PexArtifact { let artifact = PexArtifact {
url: "https://files.pythonhosted.org/packages/test.whl".to_string(), 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(), algorithm: "sha256".to_string(),
hash: "abcd1234".to_string(), hash: "abcd1234".to_string(),
is_wheel: true, is_wheel: Some(true),
}; };
let json = serde_json::to_string(&artifact).unwrap(); let json = serde_json::to_string(&artifact).unwrap();
@ -410,10 +479,10 @@ mod tests {
let requirement = PexLockedRequirement { let requirement = PexLockedRequirement {
artifacts: vec![PexArtifact { artifacts: vec![PexArtifact {
url: "https://files.pythonhosted.org/packages/test.whl".to_string(), 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(), algorithm: "sha256".to_string(),
hash: "abcd1234".to_string(), hash: "abcd1234".to_string(),
is_wheel: true, is_wheel: Some(true),
}], }],
project_name: "test-package".to_string(), project_name: "test-package".to_string(),
requires_dists: vec!["dependency>=1.0".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 // Test the git URL detection and filename generation logic
let git_url = "git+https://github.com/user/repo.git"; let git_url = "git+https://github.com/user/repo.git";
assert!(git_url.starts_with("git+")); assert!(git_url.starts_with("git+"));
let package_name = "test-package"; let package_name = "test-package";
let version = "1.5.3"; let version = "1.5.3";
let expected_filename = format!("{}-{}.tar.gz", package_name, version); let expected_filename = format!("{}-{}.tar.gz", package_name, version);
assert_eq!(expected_filename, "test-package-1.5.3.tar.gz"); 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] #[test]
fn test_platform_tag_windows_detection() { fn test_platform_tag_windows_detection() {
// Test that we can properly identify Windows platform tags // Test that we can properly identify Windows platform tags
@ -476,7 +569,9 @@ mod tests {
// Test non-Windows tags // Test non-Windows tags
let non_windows_tags = vec![ let non_windows_tags = vec![
PlatformTag::Any, 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 { for tag in non_windows_tags {
@ -487,7 +582,11 @@ mod tests {
| PlatformTag::WinArm64 | PlatformTag::WinArm64
| PlatformTag::WinIa64 | 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(); let json = pex_lock.to_json().unwrap();
// Verify all required fields are present // Verify all required fields are present
let required_fields = [ let required_fields = [
"allow_builds", "allow_prereleases", "allow_wheels", "build_isolation", "allow_builds",
"constraints", "elide_unused_requires_dist", "excluded", "locked_resolves", "allow_prereleases",
"only_builds", "only_wheels", "overridden", "path_mappings", "allow_wheels",
"pex_version", "pip_version", "prefer_older_binary", "requirements", "build_isolation",
"requires_python", "resolver_version", "style", "target_systems", "constraints",
"transitive", "use_pep517", "use_system_time" "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 { for field in required_fields {
assert!(json.contains(&format!("\"{}\"", field)), assert!(
"Missing required field: {}", field); json.contains(&format!("\"{}\"", field)),
"Missing required field: {}",
field
);
} }
} }