Skip to content

Commit

Permalink
Expand the templating language used by pinning
Browse files Browse the repository at this point in the history
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 <jrray@jrray.org>
  • Loading branch information
jrray committed May 14, 2024
1 parent 4101815 commit 8894a64
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 11 deletions.
65 changes: 60 additions & 5 deletions crates/spk-schema/crates/ident/src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions crates/spk-schema/crates/ident/src/request_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 39 additions & 6 deletions docs/ref/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 8894a64

Please sign in to comment.