Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions crates/pixi_build_type_conversions/src/project_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,18 @@ fn to_pixi_spec_v1(
itertools::Either::Left(source) => {
let source = match source.location {
pixi_spec::SourceLocationSpec::Url(url_source_spec) => {
let pixi_spec::UrlSourceSpec { url, md5, sha256 } = url_source_spec;
pbt::SourcePackageSpecV1::Url(pbt::UrlSpecV1 { url, md5, sha256 })
let pixi_spec::UrlSourceSpec {
url,
subdirectory,
md5,
sha256,
} = url_source_spec;
pbt::SourcePackageSpecV1::Url(pbt::UrlSpecV1 {
url,
subdirectory,
md5,
sha256,
})
}
pixi_spec::SourceLocationSpec::Git(git_spec) => {
let pixi_spec::GitSpec {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
source: crates/pixi_build_type_conversions/src/project_model.rs
expression: project_model
---
{
"version": "1",
"data": {
"name": "sdl_example",
"version": "0.1.0",
"description": null,
"authors": null,
"license": null,
"licenseFile": null,
"readme": null,
"homepage": null,
"repository": null,
"documentation": null,
"targets": {
"defaultTarget": {
"hostDependencies": {
"sdl2": {
"binary": {
"version": ">=2.26.5,<3.0",
"build": null,
"buildNumber": null,
"fileName": null,
"channel": null,
"subdir": null,
"md5": null,
"sha256": null,
"url": null,
"license": null
}
}
},
"buildDependencies": {},
"runDependencies": {}
},
"targets": {}
}
}
}
15 changes: 14 additions & 1 deletion crates/pixi_build_types/src/project_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,10 @@ pub struct UrlSpecV1 {
/// The URL of the package
pub url: Url,

/// The subdirectory of the archive to use, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub subdirectory: Option<String>,

/// The md5 hash of the package
#[serde_as(as = "Option<rattler_digest::serde::SerializableHash::<rattler_digest::Md5>>")]
pub md5: Option<Md5Hash>,
Expand All @@ -289,6 +293,9 @@ impl std::fmt::Debug for UrlSpecV1 {
if let Some(sha256) = &self.sha256 {
debug_struct.field("sha256", &format!("{sha256:x}"));
}
if let Some(subdir) = &self.subdirectory {
debug_struct.field("subdirectory", subdir);
}
debug_struct.finish()
}
}
Expand Down Expand Up @@ -546,11 +553,17 @@ impl Hash for UrlSpecV1 {
/// field configurations produce different hashes while maintaining
/// forward/backward compatibility.
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let UrlSpecV1 { url, md5, sha256 } = self;
let UrlSpecV1 {
url,
md5,
sha256,
subdirectory,
} = self;

StableHashBuilder::<H>::new()
.field("md5", md5)
.field("sha256", sha256)
.field("subdirectory", subdirectory)
.field("url", url)
.finish(state);
}
Expand Down
1 change: 1 addition & 0 deletions crates/pixi_command_dispatcher/src/build/conversion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub fn from_source_spec_v1(source: SourcePackageSpecV1) -> pixi_spec::SourceSpec
SourcePackageSpecV1::Url(url) => pixi_spec::SourceSpec {
location: SourceLocationSpec::Url(pixi_spec::UrlSourceSpec {
url: url.url,
subdirectory: url.subdirectory,
md5: url.md5,
sha256: url.sha256,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,7 @@ impl CommandDispatcher {
SourceLocationSpec::Url(url) => {
self.pin_and_checkout_url(UrlSpec {
url: url.url,
subdirectory: url.subdirectory,
md5: url.md5,
sha256: url.sha256,
})
Expand Down
30 changes: 21 additions & 9 deletions crates/pixi_command_dispatcher/src/command_dispatcher/url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,26 @@ impl CommandDispatcher {
&self,
url_spec: UrlSpec,
) -> Result<SourceCheckout, CommandDispatcherError<SourceCheckoutError>> {
let subdirectory = url_spec.subdirectory.clone();
// Fetch the url in the background
let UrlCheckout { pinned_url, dir } = self
let UrlCheckout {
mut pinned_url,
dir,
} = self
.checkout_url(url_spec)
.await
.map_err(|err| err.map(SourceCheckoutError::from))?;

let path = if let Some(subdir) = subdirectory {
pinned_url.subdirectory = Some(subdir.clone());
dir.join(subdir)
} else {
pinned_url.subdirectory = None;
dir
};

Ok(SourceCheckout {
path: dir,
path,
pinned: PinnedSourceSpec::Url(pinned_url),
})
}
Expand All @@ -62,6 +74,7 @@ impl CommandDispatcher {
) -> Result<SourceCheckout, CommandDispatcherError<SourceCheckoutError>> {
let url_spec = UrlSpec {
url: pinned_url_spec.url.clone(),
subdirectory: pinned_url_spec.subdirectory.clone(),
md5: pinned_url_spec.md5,
sha256: Some(pinned_url_spec.sha256),
};
Expand All @@ -71,13 +84,12 @@ impl CommandDispatcher {
.await
.map_err(|err| err.map(SourceCheckoutError::from))?;

// TODO: Similar to TODO above.
// let path = if let Some(subdir) = url_spec.source.subdirectory.as_ref() {
// fetch.path().join(subdir)
// } else {
// fetch.into_path()
// };
let path = fetch.into_path();
let base_path = fetch.into_path();
let path = if let Some(subdir) = pinned_url_spec.subdirectory.as_deref() {
base_path.join(subdir)
} else {
base_path
};

Ok(SourceCheckout {
path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ impl InstantiateToolEnvironmentSpec {
else {
return Err(CommandDispatcherError::Failed(
InstantiateToolEnvironmentError::NoMatchingBackends {
build_backend: self.requirement,
build_backend: Box::new(self.requirement),
},
));
};
Expand Down Expand Up @@ -328,6 +328,6 @@ pub enum InstantiateToolEnvironmentError {
"Modify the requirements on `{}` or contact the maintainers to ensure a dependency on `{}` is added.", .build_backend.0.as_normalized(), PIXI_BUILD_API_VERSION_NAME.as_normalized()
))]
NoMatchingBackends {
build_backend: (PackageName, PixiSpec),
build_backend: Box<(PackageName, PixiSpec)>,
},
}
8 changes: 8 additions & 0 deletions crates/pixi_command_dispatcher/src/source_build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ impl SourceBuildSpec {
pinned_git.source.subdirectory = git_spec.subdirectory.clone();
}
}
if let (Some(PinnedSourceSpec::Url(pinned_url)), Some(SourceLocationSpec::Url(url_spec))) = (
build_source.as_mut(),
discovered_backend.init_params.build_source.clone(),
) {
if pinned_url.subdirectory.is_none() {
pinned_url.subdirectory = url_spec.subdirectory.clone();
}
}

// Here we have to get path in which we will run build. We have those options in order of decreasing priority:
// 1. Lock file `package_build_source`. Since we're running lock file update before building package it should pin source in there.
Expand Down
48 changes: 48 additions & 0 deletions crates/pixi_command_dispatcher/tests/integration/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@ pub async fn pin_and_checkout_url_reuses_cached_checkout() {
// Since we have the same expected hash we expect to return existing archive.
let spec = UrlSpec {
url: "https://example.com/archive.tar.gz".parse().unwrap(),
subdirectory: None,
md5: None,
sha256: Some(sha),
};
Expand All @@ -829,6 +830,49 @@ pub async fn pin_and_checkout_url_reuses_cached_checkout() {
}
}

#[tokio::test]
pub async fn pin_and_checkout_url_honors_subdirectory() {
let tempdir = tempfile::tempdir().unwrap();
let cache_dirs = CacheDirs::new(tempdir.path().join("pixi-cache"));
let archive = tempfile::tempdir().unwrap();
let url = file_url_for_test(&archive, "subdir.zip");

let dispatcher = CommandDispatcher::builder()
.with_cache_dirs(cache_dirs)
.with_executor(Executor::Serial)
.finish();

let spec = UrlSpec {
url,
subdirectory: Some("text.txt".to_string()),
md5: None,
sha256: None,
};

let checkout = dispatcher
.pin_and_checkout_url(spec)
.await
.expect("url checkout should succeed");

assert!(
checkout.path.ends_with("text.txt"),
"expected checkout path to end with subdirectory"
);
let Some(parent) = checkout.path.parent() else {
panic!("checkout with subdirectory should have a parent directory");
};
assert!(
parent.join("text.txt") == checkout.path,
"expected checkout path to be parent + subdirectory"
);
match checkout.pinned {
PinnedSourceSpec::Url(pinned) => {
assert_eq!(pinned.subdirectory.as_deref(), Some("text.txt"));
}
other => panic!("expected url pinned spec, got {other:?}"),
};
}

#[tokio::test]
pub async fn pin_and_checkout_url_reports_sha_mismatch_from_concurrent_request() {
let tempdir = tempfile::tempdir().unwrap();
Expand All @@ -843,11 +887,13 @@ pub async fn pin_and_checkout_url_reports_sha_mismatch_from_concurrent_request()

let good_spec = UrlSpec {
url: url.clone(),
subdirectory: None,
md5: None,
sha256: None,
};
let bad_spec = UrlSpec {
url,
subdirectory: None,
md5: None,
sha256: Some(Sha256::digest(b"pixi-url-bad-sha")),
};
Expand Down Expand Up @@ -880,6 +926,7 @@ pub async fn pin_and_checkout_url_validates_cached_results() {

let spec = UrlSpec {
url: url.clone(),
subdirectory: None,
md5: None,
sha256: None,
};
Expand All @@ -891,6 +938,7 @@ pub async fn pin_and_checkout_url_validates_cached_results() {

let bad_spec = UrlSpec {
url: url.clone(),
subdirectory: None,
md5: None,
sha256: Some(Sha256::digest(b"pixi-url-bad-cache")),
};
Expand Down
6 changes: 4 additions & 2 deletions crates/pixi_core/src/lock_file/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ enum UpdateError {
#[error("the lockfile is not up-to-date with requested environment: '{}'", .0.fancy_display())]
LockFileMissingEnv(EnvironmentName),
#[error("some information from the lockfile could not be parsed")]
ParseLockFileError(#[from] ParseLockFileError),
ParseLockFileError(#[from] Box<ParseLockFileError>),
}

#[derive(Debug, Error, Diagnostic)]
Expand All @@ -408,7 +408,9 @@ pub enum SolveCondaEnvironmentError {
source: Box<CommandDispatcherError<SolvePixiEnvironmentError>>,
},

#[error("failed to map conda packages to their PyPI equivalents. This mapping is required when using PyPI dependencies alongside conda packages.")]
#[error(
"failed to map conda packages to their PyPI equivalents. This mapping is required when using PyPI dependencies alongside conda packages."
)]
#[diagnostic(transparent)]
PypiMappingFailed(#[source] Box<dyn Diagnostic + Send + Sync + 'static>),

Expand Down
37 changes: 33 additions & 4 deletions crates/pixi_manifest/src/toml/build_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,10 +198,25 @@ impl<'de> toml_span::Deserialize<'de> for TomlPackageBuild {
let additional_dependencies: UniquePackageMap =
th.optional("additional-dependencies").unwrap_or_default();

let source = th
.optional_s::<TomlLocationSpec>("source")
.map(spec_from_spanned_toml_location)
.transpose()?;
let source = match th.optional_s::<TomlLocationSpec>("source") {
Some(spanned) => {
let span = spanned.span;
let source_spec = spec_from_spanned_toml_location(spanned)?;
if let SourceLocationSpec::Url(url_spec) = &source_spec {
if url_spec.md5.is_some() {
return Err(DeserError::from(Error {
kind: toml_span::ErrorKind::Custom(Cow::Owned(
"package.build.source url md5 is not supported".to_string(),
)),
span,
line_info: None,
}));
}
}
Some(source_spec)
}
None => None,
};

// Try the new "config" key first, then fall back to deprecated "configuration"
let configuration = if let Some((_, mut value)) = th.take("config") {
Expand Down Expand Up @@ -344,6 +359,20 @@ mod test {
assert_snapshot!(expect_parse_failure(""));
}

#[test]
fn package_build_source_rejects_url_md5() {
let err = expect_parse_failure(
r#"
backend = { name = "foobar", version = "*" }
source = { url = "https://example.com/archive.zip", md5 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }
"#,
);
assert!(
err.contains("package.build.source url md5 is not supported"),
"{err}"
);
}

#[test]
fn test_missing_name() {
assert_snapshot!(expect_parse_failure(
Expand Down
2 changes: 1 addition & 1 deletion crates/pixi_record/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ pub enum ParseLockFileError {
InvalidRecordUrl(UrlOrPath, #[source] file_url::FileURLParseError),

#[error(transparent)]
PinnedSourceSpecError(#[from] pinned_source::ParseError),
PinnedSourceSpecError(#[from] Box<pinned_source::ParseError>),
}

impl Matches<PixiRecord> for NamelessMatchSpec {
Expand Down
Loading
Loading