From 96fdbc8ba53c706f4d5cb5b6b02b21745e9010c2 Mon Sep 17 00:00:00 2001 From: J Robert Ray Date: Tue, 14 May 2024 15:49:30 -0700 Subject: [PATCH] Expand the templating language used by pinning Support esoteric use cases by giving control over whether pre- or post- release components are added to the rendered pin, or more straighforwardly, allow 'v' or 'V' to be used to expand the base version or full version of the target package, respectively. For example, say you want to create an install requirement that will exactly match the version of a package used at build time. Using `fromBuildEnv: true` does not do that. Using `fromBuildEnv: ==x.x.x` does not render any pre- or post-release components. Now it is possible to put `fromBuildEnv: ==V` which would render into, e.g., `==1.2.3+r.1`. Signed-off-by: J Robert Ray --- crates/spk-schema/crates/ident/src/request.rs | 67 +++++++++++++++++-- .../crates/ident/src/request_test.rs | 19 ++++++ docs/ref/spec.md | 45 +++++++++++-- 3 files changed, 119 insertions(+), 12 deletions(-) diff --git a/crates/spk-schema/crates/ident/src/request.rs b/crates/spk-schema/crates/ident/src/request.rs index 93c52adb76..7a56d45afa 100644 --- a/crates/spk-schema/crates/ident/src/request.rs +++ b/crates/spk-schema/crates/ident/src/request.rs @@ -808,7 +808,7 @@ impl PkgRequest { fn rendered_to_pkgrequest(&self, rendered: Vec) -> Result { let mut new = self.clone(); new.pin = None; - new.pkg.version = VersionFilter::from_str(&rendered.into_iter().collect::())?; + new.pkg.version = VersionFilter::from_str(dbg!(&rendered.into_iter().collect::()))?; Ok(new) } @@ -834,14 +834,69 @@ impl PkgRequest { self.rendered_to_pkgrequest(rendered) } Some(pin) => { - let mut digits = pkg.version().parts.iter().chain(std::iter::repeat(&0)); + enum ScannerMode { + Base, + Pre, + Post, + } + let mut scanner_mode = ScannerMode::Base; + let version = pkg.version(); + let mut digits = version.parts.iter().chain(std::iter::repeat(&0)); + let mut rendered = Vec::with_capacity(pin.len()); for char in pin.chars() { - if char == 'x' { - rendered.extend(digits.next().unwrap().to_string().chars()); - } else { - rendered.push(char); + match (char, &scanner_mode) { + ('x', ScannerMode::Base) => { + rendered.extend(digits.next().unwrap().to_string().chars()); + } + ('x', ScannerMode::Pre) => { + return Err(Error::String( + "'x' in pre-release position not supported; try 'X' instead" + .to_string(), + )); + } + ('x', ScannerMode::Post) => { + return Err(Error::String( + "'x' in post-release position not supported; try 'X' instead" + .to_string(), + )); + } + ('X', ScannerMode::Pre) => { + rendered.extend(version.pre.to_string().chars()); + } + ('X', ScannerMode::Post) => { + rendered.extend(version.post.to_string().chars()); + } + ('v', ScannerMode::Base) => { + rendered.extend(version.base_normalized().chars()); + } + ('V', ScannerMode::Base) => { + rendered.extend(version.to_string().chars()); + } + (x, _) => { + match x { + '-' => scanner_mode = ScannerMode::Pre, + '+' => scanner_mode = ScannerMode::Post, + _ => {} + }; + rendered.push(x); + } + } + } + + loop { + // Remove trailing '+', e.g., if `+X` was used but the package + // had no post-release components. + if rendered.last() == Some(&'+') { + rendered.pop(); + continue; + } + // Similarly, remove any trailing '-'. + if rendered.last() == Some(&'-') { + rendered.pop(); + continue; } + break; } self.rendered_to_pkgrequest(rendered) diff --git a/crates/spk-schema/crates/ident/src/request_test.rs b/crates/spk-schema/crates/ident/src/request_test.rs index 9f130e49db..7ba0af5ed4 100644 --- a/crates/spk-schema/crates/ident/src/request_test.rs +++ b/crates/spk-schema/crates/ident/src/request_test.rs @@ -314,6 +314,25 @@ fn test_var_request_pinned_roundtrip() { #[case("1.2.3.4.5", "API", "API:1.2.3.4.5")] #[case("1.2.3", "API:x.x", "API:1.2")] #[case("1.2.3", "true", "Binary:1.2.3")] +#[case::v_expands_into_base_version("1.2.3+r.1", "v", "1.2.3")] +#[case::capital_v_expands_into_full_version("1.2.3+r.1", "V", "1.2.3+r.1")] +#[case::capital_x_in_post_position_expands_all_post_releases( + "1.2.3+r.1,s.2", + "x.x+X", + "1.2+r.1,s.2" +)] +#[case::capital_x_in_post_position_with_no_actual_post_release("1.2.3", "x.x+X", "1.2")] +#[case::capital_x_in_pre_and_post_position_with_no_actual_post_release_expected_order( + "1.2.3", "x.x-X+X", "1.2" +)] +#[case::capital_x_in_pre_and_post_position_with_no_actual_post_release_unexpected_order( + "1.2.3", "x.x+X-X", "1.2" +)] +#[case::v_in_post_release_do_not_expand_to_version("1.2.3+v.1", "x.x.x+v.2", "1.2.3+v.2")] +#[should_panic] +#[case::x_in_pre_release_position_is_not_allowed("1.2.3-r.1", "x.x-x", "n/a")] +#[should_panic] +#[case::x_in_post_release_position_is_not_allowed("1.2.3+r.1", "x.x+x", "n/a")] fn test_pkg_request_pin_rendering( #[case] version: &str, #[case] pin: &str, diff --git a/docs/ref/spec.md b/docs/ref/spec.md index f0422a3297..4037381fa2 100644 --- a/docs/ref/spec.md +++ b/docs/ref/spec.md @@ -346,12 +346,45 @@ A build option can be one of [VariableRequest](#variablerequest), or [PackageReq #### PackageRequest | Field | Type | Description | -| ------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| pkg | _[`RangeIdentifier`](#rangeidentifier)_ | Specifies a desired package, components and acceptable version range. | -| prereleasePolicy | _[PreReleasePolicy](#prereleasepolicy)_ | Defines how pre-release versions should be handled when resolving this request | -| inclusionPolicy | _[InclusionPolicy](#inclusionpolicy)_ | Defines when the requested package should be included in the environment | -| fromBuildEnv | _str_ or _bool_ | Either true, or a template to generate this request from using the version of the package that was resolved into the build environment. This template takes the form`x.x.x`, where any _x_ is replaced by digits in the version number. For example, if `python/2.7.5` is in the build environment, the template `~x.x` would become `~2.7`. The special values of `Binary` and `API` can be used to request a binary or api compatible package to the one in the build environment, respectively. For Example, if `mypkg/1.2.3.4` is in the build environment, the template `API` would become `API:1.2.3.4`. A value of `true` works the same as `Binary`. | -| ifPresentInBuildEnv | _bool_ | Either true or false; if true, then `fromBuildEnv` only applies if the package was present in the build environment. This allows different variants to have different runtime requirements. | +| ------------------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| pkg | _[`RangeIdentifier`](#rangeidentifier)_ | Specifies a desired package, components and acceptable version range. | +| prereleasePolicy | _[PreReleasePolicy](#prereleasepolicy)_ | Defines how pre-release versions should be handled when resolving this request | +| inclusionPolicy | _[InclusionPolicy](#inclusionpolicy)_ | Defines when the requested package should be included in the environment | +| fromBuildEnv | _str_ or _bool_ | Either true, or a template to generate this request from using the version of the package that was resolved into the build environment. See [FromBuildEnvTemplate](#frombuildenvtemplate) for more information. | +| ifPresentInBuildEnv | _bool_ | Either true or false; if true, then `fromBuildEnv` only applies if the package was present in the build environment. This allows different variants to have different runtime requirements. | + +##### FromBuildEnvTemplate + +This template takes the form of any valid version range expression, but any +_x_ characters that appear are replaced by digits in the version number. For +example, if `python/2.7.5` is in the build environment, the template `~x.x` +would become `~2.7`. The special values of `Binary` and `API` can be used to +request a binary or API compatible package to the one in the build environment, +respectively. For Example, if `mypkg/1.2.3.4` is in the build environment, the +template `API` would become `API:1.2.3.4`. A value of `true` works the same as +`Binary`. + +###### Advanced Usage + +Besides `x`, other characters are available: + +- 'v' - Expands to the full base version of the package. +- 'V' - Expands to the full version of the package, including any pre/post + release information. +- 'X' - Expands all the pre or post release information, depending on its + position in the template. It's okay if the package does not have any pre or + post release components. + +Examples: + +If the target package is `python/3.9.5-alpha.1+post.1,hotfix.2`, then: + +- `~x.x` -> `~3.9` +- `~v` -> `~3.9.5` +- `~V` -> `~3.9.5-alpha.1+post.1,hotfix.2` +- `~x.x-X` -> `~3.9-alpha.1` +- `~x.x+X` -> `~3.9+hotfix.2,post.1` +- `~x.x-X+X` -> `~3.9-alpha1+hotfix.2,post.1` #### RangeIdentifier