Skip to content

Commit f33f6a4

Browse files
committed
feat: support pkg-config and version metadata
1 parent 85acd4e commit f33f6a4

6 files changed

Lines changed: 207 additions & 25 deletions

File tree

.github/workflows/ci.yml

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
- run: cargo clippy --all-targets --all-features -- -D warnings
7272

7373
test:
74-
name: Test
74+
name: Test (vendored)
7575
runs-on: ubuntu-latest
7676
steps:
7777
- uses: actions/checkout@v6
@@ -80,3 +80,23 @@ jobs:
8080
- uses: dtolnay/rust-toolchain@stable
8181
- run: sudo apt-get update && sudo apt-get install -y libclang-dev
8282
- run: cargo test
83+
84+
test-system:
85+
name: Test (system libversion)
86+
runs-on: ubuntu-latest
87+
steps:
88+
- uses: actions/checkout@v6
89+
with:
90+
submodules: true
91+
- uses: dtolnay/rust-toolchain@stable
92+
- run: sudo apt-get update && sudo apt-get install -y cmake libclang-dev pkg-config
93+
- name: Build and install libversion for pkg-config mode
94+
run: |
95+
cmake -S libversion -B "$RUNNER_TEMP/libversion-build" -DCMAKE_INSTALL_PREFIX="$RUNNER_TEMP/libversion-install"
96+
cmake --build "$RUNNER_TEMP/libversion-build"
97+
cmake --install "$RUNNER_TEMP/libversion-build"
98+
- name: Test without vendored feature
99+
run: cargo test --no-default-features
100+
env:
101+
PKG_CONFIG_PATH: ${{ runner.temp }}/libversion-install/lib/pkgconfig:${{ runner.temp }}/libversion-install/lib64/pkgconfig
102+
LD_LIBRARY_PATH: ${{ runner.temp }}/libversion-install/lib:${{ runner.temp }}/libversion-install/lib64

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,16 @@ license = "MIT"
99
repository = "https://github.com/DUpdateSystem/libversion-sys"
1010
homepage = "https://github.com/DUpdateSystem/libversion-sys"
1111
documentation = "https://docs.rs/libversion-sys"
12+
readme = "README.md"
13+
keywords = ["ffi", "version", "comparison", "bindings"]
14+
categories = ["external-ffi-bindings", "algorithms"]
1215
exclude = [".github/", ".gitignore", ".gitmodules", "scripts/"]
1316

17+
[features]
18+
default = ["vendored"]
19+
vendored = []
20+
1421
[build-dependencies]
1522
cc = "1"
1623
bindgen = "0.72"
24+
pkg-config = "0.3"

README.md

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,29 @@
66

77
Rust FFI bindings to [libversion](https://github.com/repology/libversion), an advanced version string comparison library.
88

9-
The C source is included and compiled from source via CMake -- no system-level installation of libversion is required.
9+
By default the crate vendors the C source and builds it directly, so no system-level installation of libversion is required. If you prefer linking an installed system copy, disable default features and make sure `pkg-config` can find `libversion`.
1010

1111
## Usage
1212

1313
Add to `Cargo.toml`:
1414

1515
```toml
1616
[dependencies]
17-
libversion-sys = "0.1"
17+
libversion-sys = "0.2"
18+
```
19+
20+
Use the default vendored build:
21+
22+
```toml
23+
[dependencies]
24+
libversion-sys = "0.2"
25+
```
26+
27+
Or link a system-installed `libversion`:
28+
29+
```toml
30+
[dependencies]
31+
libversion-sys = { version = "0.2", default-features = false }
1832
```
1933

2034
### Safe API
@@ -46,6 +60,13 @@ let result = unsafe { ffi::version_compare2(v1.as_ptr(), v2.as_ptr()) };
4660
assert_eq!(result, -1);
4761
```
4862

63+
### Version metadata
64+
65+
```rust
66+
assert!(libversion_sys::version_atleast(3, 0, 0));
67+
assert!(!libversion_sys::version_string().is_empty());
68+
```
69+
4970
## Flags
5071

5172
| Flag | Description |
@@ -58,16 +79,24 @@ assert_eq!(result, -1);
5879
## Build requirements
5980

6081
- Rust (stable)
61-
- CMake
62-
- C compiler (gcc/clang)
82+
- C compiler (gcc/clang) for the default vendored build
6383
- libclang (for bindgen)
84+
- `pkg-config` and a system `libversion` installation when building with `default-features = false`
6485

6586
On Ubuntu/Debian:
6687

6788
```sh
68-
sudo apt-get install cmake libclang-dev
89+
sudo apt-get install libclang-dev
6990
```
7091

92+
For system linking:
93+
94+
```sh
95+
sudo apt-get install pkg-config libversion-dev
96+
```
97+
98+
`cmake` is only needed by maintainers when regenerating `generated/libversion/config.h` and `generated/libversion/export.h` after updating the vendored libversion source.
99+
71100
## License
72101

73102
MIT -- see [LICENSE](LICENSE).

build.rs

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,57 @@ use std::env;
22
use std::path::PathBuf;
33

44
fn main() {
5-
// Build libversion static library using cc.
6-
// The cmake-generated headers (config.h, export.h) are pre-committed under
7-
// generated/ — run `bash scripts/generate-headers.sh` to regenerate them
8-
// whenever the libversion submodule is updated.
9-
cc::Build::new()
10-
.file("libversion/libversion/compare.c")
11-
.file("libversion/libversion/private/compare.c")
12-
.file("libversion/libversion/private/parse.c")
13-
.include("libversion") // source headers (libversion/version.h, etc.)
14-
.include("generated") // pre-generated headers (libversion/config.h, libversion/export.h)
15-
.define("LIBVERSION_STATIC_DEFINE", None)
16-
.compile("version");
5+
let include_paths = if env::var_os("CARGO_FEATURE_VENDORED").is_some() {
6+
build_vendored();
7+
vec![PathBuf::from("generated"), PathBuf::from("libversion")]
8+
} else {
9+
find_system_lib()
10+
};
1711

18-
// Generate FFI bindings via bindgen
19-
let bindings = bindgen::Builder::default()
12+
let mut bindings = bindgen::Builder::default()
2013
.header("wrapper.h")
21-
.clang_arg("-Igenerated") // pre-generated config.h, export.h
22-
.clang_arg("-Ilibversion") // source headers
23-
.clang_arg("-DLIBVERSION_STATIC_DEFINE")
2414
.default_enum_style(bindgen::EnumVariation::Consts)
2515
.allowlist_function("version_compare.*")
2616
.allowlist_var("VERSIONFLAG_.*")
17+
.allowlist_var("LIBVERSION_VERSION.*")
2718
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
28-
.generate()
29-
.expect("Unable to generate bindings");
19+
.clang_args(
20+
include_paths
21+
.iter()
22+
.map(|path| format!("-I{}", path.display())),
23+
);
24+
25+
if env::var_os("CARGO_FEATURE_VENDORED").is_some() {
26+
bindings = bindings.clang_arg("-DLIBVERSION_STATIC_DEFINE");
27+
}
28+
29+
let bindings = bindings.generate().expect("Unable to generate bindings");
3030

3131
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
3232
bindings
3333
.write_to_file(out_path.join("bindings.rs"))
3434
.expect("Couldn't write bindings!");
3535
}
36+
37+
fn build_vendored() {
38+
// Build libversion from vendored C sources. The cmake-generated headers
39+
// (config.h, export.h) are pre-committed under generated/ so end users do
40+
// not need cmake installed for the default build.
41+
cc::Build::new()
42+
.file("libversion/libversion/compare.c")
43+
.file("libversion/libversion/private/compare.c")
44+
.file("libversion/libversion/private/parse.c")
45+
.include("libversion")
46+
.include("generated")
47+
.define("LIBVERSION_STATIC_DEFINE", None)
48+
.compile("version");
49+
}
50+
51+
fn find_system_lib() -> Vec<PathBuf> {
52+
let library = pkg_config::Config::new()
53+
.atleast_version("3.0.0")
54+
.probe("libversion")
55+
.expect("failed to find system libversion via pkg-config; enable the vendored feature or install libversion.pc");
56+
57+
library.include_paths
58+
}

src/lib.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub mod ffi {
2020
}
2121

2222
pub use ffi::{
23+
LIBVERSION_VERSION_MAJOR, LIBVERSION_VERSION_MINOR, LIBVERSION_VERSION_PATCH,
2324
VERSIONFLAG_ANY_IS_PATCH, VERSIONFLAG_LOWER_BOUND, VERSIONFLAG_P_IS_PATCH,
2425
VERSIONFLAG_UPPER_BOUND, version_compare2, version_compare4,
2526
};
@@ -79,6 +80,22 @@ pub fn compare_with_flags(v1: &str, v2: &str, v1_flags: u32, v2_flags: u32) -> O
7980
result.cmp(&0)
8081
}
8182

83+
/// Returns the libversion release string from the headers used during build.
84+
pub fn version_string() -> &'static str {
85+
std::str::from_utf8(&ffi::LIBVERSION_VERSION[..ffi::LIBVERSION_VERSION.len() - 1])
86+
.expect("libversion version string is not valid UTF-8")
87+
}
88+
89+
/// Rust equivalent of the `LIBVERSION_VERSION_ATLEAST` macro.
90+
#[allow(clippy::absurd_extreme_comparisons)]
91+
pub const fn version_atleast(major: u32, minor: u32, patch: u32) -> bool {
92+
(LIBVERSION_VERSION_MAJOR > major)
93+
|| (LIBVERSION_VERSION_MAJOR == major && LIBVERSION_VERSION_MINOR > minor)
94+
|| (LIBVERSION_VERSION_MAJOR == major
95+
&& LIBVERSION_VERSION_MINOR == minor
96+
&& LIBVERSION_VERSION_PATCH >= patch)
97+
}
98+
8299
#[cfg(test)]
83100
mod tests {
84101
use super::*;
@@ -115,11 +132,89 @@ mod tests {
115132
);
116133
}
117134

135+
#[test]
136+
fn any_is_patch_flag() {
137+
assert_eq!(compare("1.0foopatchset1", "1.0"), Ordering::Less);
138+
assert_eq!(
139+
compare_with_flags("1.0foopatchset1", "1.0", VERSIONFLAG_ANY_IS_PATCH, 0),
140+
Ordering::Greater,
141+
);
142+
}
143+
144+
#[test]
145+
fn lower_bound_flag() {
146+
assert_eq!(
147+
compare_with_flags("1.0alpha1", "1.0", 0, VERSIONFLAG_LOWER_BOUND),
148+
Ordering::Greater,
149+
);
150+
assert_eq!(
151+
compare_with_flags("0.999", "1.0", 0, VERSIONFLAG_LOWER_BOUND),
152+
Ordering::Less,
153+
);
154+
}
155+
156+
#[test]
157+
fn upper_bound_flag() {
158+
assert_eq!(
159+
compare_with_flags("1.0.1", "1.0", 0, VERSIONFLAG_UPPER_BOUND),
160+
Ordering::Less,
161+
);
162+
assert_eq!(
163+
compare_with_flags("1.1", "1.0", 0, VERSIONFLAG_UPPER_BOUND),
164+
Ordering::Greater,
165+
);
166+
}
167+
168+
#[test]
169+
#[should_panic(expected = "v1 contains interior null byte")]
170+
fn compare_rejects_interior_null() {
171+
let _ = compare("1.0\0rc1", "1.0");
172+
}
173+
174+
#[test]
175+
#[should_panic(expected = "v2 contains interior null byte")]
176+
fn compare_with_flags_rejects_interior_null() {
177+
let _ = compare_with_flags("1.0", "1.0\0rc1", 0, 0);
178+
}
179+
180+
#[test]
181+
fn version_metadata() {
182+
let parts = version_string()
183+
.split('.')
184+
.map(|part| part.parse::<u32>().unwrap())
185+
.collect::<Vec<_>>();
186+
187+
assert_eq!(
188+
parts,
189+
vec![
190+
LIBVERSION_VERSION_MAJOR,
191+
LIBVERSION_VERSION_MINOR,
192+
LIBVERSION_VERSION_PATCH,
193+
]
194+
);
195+
assert!(version_atleast(
196+
LIBVERSION_VERSION_MAJOR,
197+
LIBVERSION_VERSION_MINOR,
198+
LIBVERSION_VERSION_PATCH,
199+
));
200+
assert!(!version_string().is_empty());
201+
}
202+
118203
#[test]
119204
fn ffi_direct() {
120205
let v1 = CString::new("1.0").unwrap();
121206
let v2 = CString::new("2.0").unwrap();
122207
let result = unsafe { ffi::version_compare2(v1.as_ptr(), v2.as_ptr()) };
123208
assert_eq!(result, -1);
124209
}
210+
211+
#[test]
212+
fn ffi_compare4_direct() {
213+
let v1 = CString::new("1.0p1").unwrap();
214+
let v2 = CString::new("1.0").unwrap();
215+
let result = unsafe {
216+
ffi::version_compare4(v1.as_ptr(), v2.as_ptr(), VERSIONFLAG_P_IS_PATCH as i32, 0)
217+
};
218+
assert_eq!(result, 1);
219+
}
125220
}

0 commit comments

Comments
 (0)