Skip to content

Commit 45c8685

Browse files
sdroegegdesmott
authored andcommitted
Implement support for requiring dependency version ranges
This now supports expressions in the form * "1.2" or ">= 1.2" for at least version 1.2 * ">= 1.2, < 2.0" for at least version 1.2 and less than version 2.0 Fixes #60
1 parent 1059ca5 commit 45c8685

File tree

5 files changed

+181
-15
lines changed

5 files changed

+181
-15
lines changed

src/lib.rs

+72-15
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@
2828
//! }
2929
//! ```
3030
//!
31+
//! # Version format
32+
//!
33+
//! Versions can be expressed in the following formats
34+
//!
35+
//! * "1.2" or ">= 1.2": At least version 1.2
36+
//! * ">= 1.2, < 2.0": At least version 1.2 but less than version 2.0
37+
//!
38+
//! In the future more complicated version expressions might be supported.
39+
//!
40+
//! Note that these versions are not interpreted according to the semver rules, but based on the
41+
//! rules defined by pkg-config.
42+
//!
3143
//! # Feature-specific dependency
3244
//! You can easily declare an optional system dependency by associating it with a feature:
3345
//!
@@ -159,8 +171,8 @@
159171
//! fn main() {
160172
//! system_deps::Config::new()
161173
//! .add_build_internal("testlib", |lib, version| {
162-
//! // Actually build the library here
163-
//! system_deps::Library::from_internal_pkg_config("build/path-to-pc-file", lib, version)
174+
//! // Actually build the library here that fulfills the passed in version requirements
175+
//! system_deps::Library::from_internal_pkg_config("build/path-to-pc-file", lib, "1.2.4")
164176
//! })
165177
//! .probe()
166178
//! .unwrap();
@@ -195,6 +207,7 @@ use heck::{ToShoutySnakeCase, ToSnakeCase};
195207
use std::collections::HashMap;
196208
use std::env;
197209
use std::fmt;
210+
use std::ops::RangeBounds;
198211
use std::path::{Path, PathBuf};
199212
use std::str::FromStr;
200213

@@ -721,7 +734,18 @@ impl Config {
721734
optional = dep.optional;
722735
} else {
723736
enabled_feature_overrides.sort_by(|a, b| {
724-
version_compare::compare(&a.version, &b.version)
737+
fn min_version(r: metadata::VersionRange) -> &str {
738+
match r.start_bound() {
739+
std::ops::Bound::Unbounded => unreachable!(),
740+
std::ops::Bound::Excluded(_) => unreachable!(),
741+
std::ops::Bound::Included(b) => b,
742+
}
743+
}
744+
745+
let a = min_version(metadata::parse_version(&a.version));
746+
let b = min_version(metadata::parse_version(&b.version));
747+
748+
version_compare::compare(a, b)
725749
.expect("failed to compare versions")
726750
.ord()
727751
.expect("invalid version")
@@ -758,10 +782,11 @@ impl Config {
758782
} else {
759783
let mut config = pkg_config::Config::new();
760784
config
761-
.atleast_version(version)
762785
.print_system_libs(false)
763786
.cargo_metadata(false)
787+
.range_version(metadata::parse_version(version))
764788
.statik(statik);
789+
765790
match Self::probe_with_fallback(config, lib_name, fallback_lib_names) {
766791
Ok((lib_name, lib)) => Library::from_pkg_config(lib_name, lib),
767792
Err(e) => {
@@ -826,23 +851,55 @@ impl Config {
826851
}
827852
}
828853

829-
fn call_build_internal(&mut self, name: &str, version: &str) -> Result<Library, Error> {
854+
fn call_build_internal(&mut self, name: &str, version_str: &str) -> Result<Library, Error> {
830855
let lib = match self.build_internals.remove(name) {
831-
Some(f) => {
832-
f(name, version).map_err(|e| Error::BuildInternalClosureError(name.into(), e))?
856+
Some(f) => f(name, version_str)
857+
.map_err(|e| Error::BuildInternalClosureError(name.into(), e))?,
858+
None => {
859+
return Err(Error::BuildInternalNoClosure(
860+
name.into(),
861+
version_str.into(),
862+
))
833863
}
834-
None => return Err(Error::BuildInternalNoClosure(name.into(), version.into())),
835864
};
836865

837866
// Check that the lib built internally matches the required version
838-
match version_compare::compare(&lib.version, version) {
839-
Ok(version_compare::Cmp::Lt) => Err(Error::BuildInternalWrongVersion(
867+
let version = metadata::parse_version(version_str);
868+
fn min_version(r: metadata::VersionRange) -> &str {
869+
match r.start_bound() {
870+
std::ops::Bound::Unbounded => unreachable!(),
871+
std::ops::Bound::Excluded(_) => unreachable!(),
872+
std::ops::Bound::Included(b) => b,
873+
}
874+
}
875+
fn max_version(r: metadata::VersionRange) -> Option<&str> {
876+
match r.end_bound() {
877+
std::ops::Bound::Included(_) => unreachable!(),
878+
std::ops::Bound::Unbounded => None,
879+
std::ops::Bound::Excluded(b) => Some(*b),
880+
}
881+
}
882+
883+
let min = min_version(version.clone());
884+
if version_compare::compare(&lib.version, min) == Ok(version_compare::Cmp::Lt) {
885+
return Err(Error::BuildInternalWrongVersion(
840886
name.into(),
841887
lib.version,
842-
version.into(),
843-
)),
844-
_ => Ok(lib),
888+
version_str.into(),
889+
));
845890
}
891+
892+
if let Some(max) = max_version(version) {
893+
if version_compare::compare(&lib.version, max) == Ok(version_compare::Cmp::Ge) {
894+
return Err(Error::BuildInternalWrongVersion(
895+
name.into(),
896+
lib.version,
897+
version_str.into(),
898+
));
899+
}
900+
}
901+
902+
Ok(lib)
846903
}
847904

848905
fn has_feature(&self, feature: &str) -> bool {
@@ -1008,9 +1065,9 @@ impl Library {
10081065
/// ```
10091066
/// let mut config = system_deps::Config::new();
10101067
/// config.add_build_internal("mylib", |lib, version| {
1011-
/// // Actually build the library here
1068+
/// // Actually build the library here that fulfills the passed in version requirements
10121069
/// system_deps::Library::from_internal_pkg_config("build-dir",
1013-
/// lib, version)
1070+
/// lib, "1.2.4")
10141071
/// });
10151072
/// ```
10161073
pub fn from_internal_pkg_config<P>(

src/metadata.rs

+71
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,14 @@ impl MetaData {
242242
match value {
243243
// somelib = "1.0"
244244
toml::Value::String(ref s) => {
245+
if !validate_version(s) {
246+
return Err(MetadataError::UnexpectedVersionSetting(
247+
key.into(),
248+
name.into(),
249+
value.type_str().to_owned(),
250+
));
251+
}
252+
245253
dep.version = Some(s.clone());
246254
}
247255
toml::Value::Table(ref t) => {
@@ -267,6 +275,14 @@ impl MetaData {
267275
dep.feature = Some(s.clone());
268276
}
269277
("version", toml::Value::String(s)) => {
278+
if !validate_version(s) {
279+
return Err(MetadataError::UnexpectedVersionSetting(
280+
format!("{}.{}", p_key, name),
281+
key.into(),
282+
value.type_str().to_owned(),
283+
));
284+
}
285+
270286
dep.version = Some(s.clone());
271287
}
272288
("name", toml::Value::String(s)) => {
@@ -287,6 +303,14 @@ impl MetaData {
287303
for (k, v) in version_settings {
288304
match (k.as_str(), v) {
289305
("version", toml::Value::String(feat_vers)) => {
306+
if !validate_version(feat_vers) {
307+
return Err(MetadataError::UnexpectedVersionSetting(
308+
format!("{}.{}", p_key, name),
309+
k.into(),
310+
v.type_str().to_owned(),
311+
));
312+
}
313+
290314
builder.version = Some(feat_vers.into());
291315
}
292316
("name", toml::Value::String(feat_name)) => {
@@ -337,6 +361,53 @@ impl MetaData {
337361
}
338362
}
339363

364+
fn validate_version(version: &str) -> bool {
365+
if let Some((min, max)) = version.split_once(',') {
366+
if !min.trim_start().starts_with(">=") || !max.trim_start().starts_with('<') {
367+
return false;
368+
}
369+
370+
true
371+
} else {
372+
true
373+
}
374+
}
375+
376+
#[derive(Debug, Clone)]
377+
pub(crate) enum VersionRange<'a> {
378+
Range(std::ops::Range<&'a str>),
379+
RangeFrom(std::ops::RangeFrom<&'a str>),
380+
}
381+
382+
impl<'a> std::ops::RangeBounds<&'a str> for VersionRange<'a> {
383+
fn start_bound(&self) -> std::ops::Bound<&&'a str> {
384+
match self {
385+
VersionRange::Range(r) => r.start_bound(),
386+
VersionRange::RangeFrom(r) => r.start_bound(),
387+
}
388+
}
389+
390+
fn end_bound(&self) -> std::ops::Bound<&&'a str> {
391+
match self {
392+
VersionRange::Range(r) => r.end_bound(),
393+
VersionRange::RangeFrom(r) => r.end_bound(),
394+
}
395+
}
396+
}
397+
398+
pub(crate) fn parse_version(version: &str) -> VersionRange {
399+
if let Some((min, max)) = version.split_once(',') {
400+
// Format checked when parsing
401+
let min = min.trim_start().strip_prefix(">=").unwrap().trim();
402+
let max = max.trim_start().strip_prefix('<').unwrap().trim();
403+
VersionRange::Range(min..max)
404+
} else if let Some(min) = version.trim_start().strip_prefix(">=") {
405+
VersionRange::RangeFrom(min..)
406+
} else {
407+
VersionRange::RangeFrom(version..)
408+
}
409+
}
410+
340411
#[cfg(test)]
341412
mod tests {
342413
use super::*;

src/test.rs

+32
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,38 @@ cargo:rerun-if-env-changed=SYSTEM_DEPS_LINK
110110
);
111111
}
112112

113+
#[test]
114+
fn version_range() {
115+
let (libraries, _flags) = toml("toml-version-range", vec![]).unwrap();
116+
let testlib = libraries.get_by_name("testlib").unwrap();
117+
assert_eq!(testlib.version, "1.2.3");
118+
assert_eq!(
119+
testlib.defines.get("BADGER").unwrap().as_deref(),
120+
Some("yes")
121+
);
122+
assert!(testlib.defines.get("AWESOME").unwrap().is_none());
123+
124+
let testdata = libraries.get_by_name("testdata").unwrap();
125+
assert_eq!(testdata.version, "4.5.6");
126+
127+
assert_eq!(libraries.iter().len(), 2);
128+
}
129+
130+
#[test]
131+
fn version_range_unsatisfied() {
132+
let err = toml_err("toml-version-range-unsatisfied");
133+
134+
assert_matches!(err, Error::PkgConfig(_));
135+
136+
let err_msg = err.to_string();
137+
// pkgconf and pkg-config give different error messages
138+
if !err_msg.contains("Package 'testlib' has version '1.2.3', required version is '< 1.2'")
139+
&& !err_msg.contains("Requested 'testlib < 1.2' but version of Test Library is 1.2.3")
140+
{
141+
panic!("Error not as expected: {:?}", err);
142+
}
143+
}
144+
113145
fn toml_err(path: &str) -> Error {
114146
toml(path, vec![]).unwrap_err()
115147
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[package.metadata.system-deps]
2+
testdata = "4"
3+
testlib = { version = ">= 1, < 1.2", feature = "test-feature" }
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[package.metadata.system-deps]
2+
testdata = "4"
3+
testlib = { version = ">= 1, < 2", feature = "test-feature" }

0 commit comments

Comments
 (0)