Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract/validate packaging tool versions at compile time #241

Merged
merged 1 commit into from
Aug 7, 2024
Merged
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "python-buildpack"
edition = "2021"
rust-version = "1.79"
rust-version = "1.80"
# Disable automatic integration test discovery, since we import them in main.rs (see comment there).
autotests = false

Expand Down
78 changes: 31 additions & 47 deletions src/packaging_tool_versions.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
use serde::{Deserialize, Serialize};
use std::str;

const PIP_REQUIREMENT: &str = include_str!("../requirements/pip.txt");
const SETUPTOOLS_REQUIREMENT: &str = include_str!("../requirements/setuptools.txt");
const WHEEL_REQUIREMENT: &str = include_str!("../requirements/wheel.txt");
// We store these versions in requirements files so that Dependabot can update them.
// Each file must contain a single package specifier in the format `package==1.2.3`,
// from which we extract/validate the version substring at compile time.
const PIP_VERSION: &str = extract_requirement_version(include_str!("../requirements/pip.txt"));
const SETUPTOOLS_VERSION: &str =
extract_requirement_version(include_str!("../requirements/setuptools.txt"));
const WHEEL_VERSION: &str = extract_requirement_version(include_str!("../requirements/wheel.txt"));

/// The versions of various packaging tools used during the build.
/// These are always installed, and are independent of the chosen package manager.
Expand All @@ -19,67 +24,46 @@ pub(crate) struct PackagingToolVersions {

impl Default for PackagingToolVersions {
fn default() -> Self {
// These versions are effectively buildpack constants, however, we want Dependabot to be able
// to update them, which requires that they be in requirements files. The requirements files
// contain contents like `package==1.2.3` (and not just the package version) so we have to
// extract the version substring from it. Ideally this would be done at compile time, however,
// using const functions would require use of unsafe and lots of boilerplate, and using proc
// macros would require the overhead of adding a separate crate. As such, it ends up being
// simpler to extract the version substring at runtime. Extracting the version is technically
// fallible, however, we control the buildpack requirements files, so if they are invalid it
// can only ever be a buildpack bug, and not something a user would ever see given the unit
// and integration tests. As such, it's safe to use `.expect()` here, and doing so saves us
// from having to add user-facing error messages that users will never see.
Self {
pip_version: extract_requirement_version(PIP_REQUIREMENT)
.expect("pip requirement file must contain a valid version"),
setuptools_version: extract_requirement_version(SETUPTOOLS_REQUIREMENT)
.expect("setuptools requirement file must contain a valid version"),
wheel_version: extract_requirement_version(WHEEL_REQUIREMENT)
.expect("wheel requirement file must contain a valid version"),
pip_version: PIP_VERSION.to_string(),
setuptools_version: SETUPTOOLS_VERSION.to_string(),
wheel_version: WHEEL_VERSION.to_string(),
}
}
}

/// Extract the version substring from an exact-version requirement specifier (such as `foo==1.2.3`).
/// This function should only be used to extract the version constants from the buildpack's own
/// requirements files, which are controlled by us and don't require a full PEP 508 version parser.
fn extract_requirement_version(requirement: &str) -> Option<String> {
match requirement.split("==").collect::<Vec<_>>().as_slice() {
&[_, version] => Some(version.trim().to_string()),
_ => None,
// Extract the version substring from an exact-version package specifier (such as `foo==1.2.3`).
// This function should only be used to extract the version constants from the buildpack's own
// requirements files, which are controlled by us and don't require a full PEP 508 version parser.
// Since this is a `const fn` we cannot use iterators, most methods on `str`, `Result::expect` etc.
const fn extract_requirement_version(requirement: &'static str) -> &'static str {
let mut bytes = requirement.as_bytes();
while let [_, rest @ ..] = bytes {
if let [b'=', b'=', version @ ..] = rest {
if let Ok(version) = str::from_utf8(version.trim_ascii()) {
return version;
}
break;
}
bytes = rest;
}
// This is safe, since this function is only used at compile time.
panic!("Requirement must be in the format: 'package==X.Y.Z'");
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn default_packaging_tool_versions() {
// If the versions in the buildpack's `requirements/*.txt` files are invalid, this will panic.
PackagingToolVersions::default();
}

#[test]
fn extract_requirement_version_valid() {
assert_eq!(
extract_requirement_version("some_package==1.2.3"),
Some("1.2.3".to_string())
);
assert_eq!(
extract_requirement_version("\nsome_package == 1.2.3\n"),
Some("1.2.3".to_string())
);
assert_eq!(extract_requirement_version("package==1.2.3"), "1.2.3");
assert_eq!(extract_requirement_version("\npackage == 0.12\n"), "0.12");
}

#[test]
#[should_panic(expected = "Requirement must be in the format")]
fn extract_requirement_version_invalid() {
assert_eq!(extract_requirement_version("some_package"), None);
assert_eq!(extract_requirement_version("some_package=<1.2.3"), None);
assert_eq!(
extract_requirement_version("some_package==1.2.3\nanother_package==4.5.6"),
None
);
extract_requirement_version("package=<1.2.3");
}
}