diff --git a/crates/spk-schema/crates/ident/src/request.rs b/crates/spk-schema/crates/ident/src/request.rs index 93c52adb7..c899fc16c 100644 --- a/crates/spk-schema/crates/ident/src/request.rs +++ b/crates/spk-schema/crates/ident/src/request.rs @@ -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 9f130e49d..7ba0af5ed 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 f0422a329..4037381fa 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