diff --git a/Cargo.toml b/Cargo.toml index cca6f12..58d6d8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/src/packaging_tool_versions.rs b/src/packaging_tool_versions.rs index 8ae44a3..5dbf972 100644 --- a/src/packaging_tool_versions.rs +++ b/src/packaging_tool_versions.rs @@ -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. @@ -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 { - match requirement.split("==").collect::>().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"); } }