Skip to main content

rnp_src/
links.rs

1//! Shared types and constants between `rnp-src`'s build.rs and its lib.rs.
2//!
3//! Both files path-include this module so the links contract has a single
4//! source of truth and the pure logic is unit-testable from lib.rs.
5
6use std::path::{Path, PathBuf};
7
8// ---------------------------------------------------------------------
9// links contract — single source of truth for the cargo:foo=… ↔
10// DEP_RNP_FOO env-var convention used between this crate's build.rs and
11// rnp-rs's build.rs.
12// ---------------------------------------------------------------------
13
14/// The `links = "rnp"` Cargo prefix that downstream build scripts see
15/// on env vars emitted by this crate.
16pub const LINKS_ENV_PREFIX: &str = "DEP_RNP_";
17
18/// Suffix appended to a dependency name to form its lib-dir env var.
19/// `botan` → `DEP_RNP_BOTAN_LIB_DIR`.
20pub const LINKS_LIB_DIR_SUFFIX: &str = "_LIB_DIR";
21
22/// All dependency names whose `<prefix>/lib` rnp-src emits via
23/// `cargo:<name>_lib_dir=<path>`. Adding a dep here makes both the
24/// emitter (`Deps::emit_cargo_paths`) and the consumer (`rnp-rs/build.rs`)
25/// pick it up automatically.
26pub const DEPS: &[&str] = &["botan", "jsonc", "zlib", "bzip2"];
27
28/// Compose the env-var name rnp-rs should read for a given dep's lib dir.
29///
30/// Cargo uppercases the entire env var name when surfacing `cargo:key=value`
31/// directives as `DEP_<LINKS>_<KEY>` env vars, so we uppercase too to match.
32pub fn lib_dir_env_var(name: &str) -> String {
33    format!("{LINKS_ENV_PREFIX}{name}{LINKS_LIB_DIR_SUFFIX}").to_uppercase()
34}
35
36// ---------------------------------------------------------------------
37// Deps — collection of per-dep install prefixes.
38// ---------------------------------------------------------------------
39
40#[derive(Debug, Clone, Default)]
41pub struct Deps {
42    items: Vec<Dep>,
43}
44
45#[derive(Debug, Clone)]
46pub struct Dep {
47    pub name: &'static str,
48    pub prefix: PathBuf,
49}
50
51impl Deps {
52    pub fn new() -> Self {
53        Self { items: Vec::new() }
54    }
55
56    pub fn push(&mut self, name: &'static str, prefix: PathBuf) {
57        assert!(
58            DEPS.contains(&name),
59            "rnp-src: Deps::push({name}) — name not in DEPS; update src/links.rs first"
60        );
61        self.items.push(Dep { name, prefix });
62    }
63
64    pub fn by_name(&self, name: &str) -> Option<&Dep> {
65        self.items.iter().find(|d| d.name == name)
66    }
67
68    /// Semicolon-joined list of dep prefixes, as cmake's CMAKE_PREFIX_PATH expects.
69    pub fn cmake_prefix_path(&self) -> String {
70        self.items
71            .iter()
72            .map(|d| d.prefix.display().to_string())
73            .collect::<Vec<_>>()
74            .join(";")
75    }
76
77    /// Emit `cargo:<name>_lib_dir=<prefix>/lib` lines so rnp-rs can find
78    /// each dep's static library at link time.
79    pub fn emit_cargo_paths(&self) {
80        for dep in &self.items {
81            println!(
82                "cargo:{}_lib_dir={}",
83                dep.name,
84                dep.prefix.join("lib").display()
85            );
86        }
87    }
88
89    pub fn iter(&self) -> impl Iterator<Item = &Dep> {
90        self.items.iter()
91    }
92}
93
94// ---------------------------------------------------------------------
95// CmakeDep — config-driven description of a cmake-built tarball dep.
96// ---------------------------------------------------------------------
97
98#[derive(Debug, Clone)]
99pub struct CmakeDep {
100    pub name: &'static str,
101    pub version: &'static str,
102    /// `format!`-style template; `{version}` is substituted at runtime.
103    pub url_template: &'static str,
104    /// Extra `-D` flags beyond the standard CMAKE_INSTALL_PREFIX /
105    /// BUILD_SHARED_LIBS / CMAKE_BUILD_TYPE.
106    pub extra_cmake_args: &'static [&'static str],
107    /// cmake policy version floor; some old libs need <3.5.
108    pub cmake_policy_minimum: Option<&'static str>,
109}
110
111impl CmakeDep {
112    pub fn source_dir(&self, src_root: &Path) -> PathBuf {
113        src_root.join(format!("{}-{}", self.name, self.version))
114    }
115
116    pub fn build_dir(&self, src_root: &Path) -> PathBuf {
117        src_root.join(format!("{}-build", self.name))
118    }
119
120    pub fn url(&self) -> String {
121        self.url_template.replace("{version}", self.version)
122    }
123}
124
125/// Bundled cmake deps rnp-src knows how to build.
126pub const JSON_C: CmakeDep = CmakeDep {
127    name: "json-c",
128    version: "0.17",
129    url_template: "https://s3.amazonaws.com/json-c_releases/releases/json-c-{version}.tar.gz",
130    extra_cmake_args: &["-DBUILD_TESTING=OFF"],
131    cmake_policy_minimum: Some("3.5"),
132};
133
134pub const ZLIB: CmakeDep = CmakeDep {
135    name: "zlib",
136    version: "1.3.1",
137    url_template: "https://github.com/madler/zlib/releases/download/v{version}/zlib-{version}.tar.gz",
138    extra_cmake_args: &[],
139    cmake_policy_minimum: None,
140};
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn lib_dir_env_var_for_botan() {
148        assert_eq!(lib_dir_env_var("botan"), "DEP_RNP_BOTAN_LIB_DIR");
149    }
150
151    #[test]
152    fn lib_dir_env_var_for_zlib() {
153        assert_eq!(lib_dir_env_var("zlib"), "DEP_RNP_ZLIB_LIB_DIR");
154    }
155
156    #[test]
157    fn all_deps_produce_well_formed_env_vars() {
158        for &name in DEPS {
159            let var = lib_dir_env_var(name);
160            assert!(var.starts_with(LINKS_ENV_PREFIX));
161            assert!(var.ends_with(LINKS_LIB_DIR_SUFFIX));
162        }
163    }
164
165    #[test]
166    fn deps_cmake_prefix_path_joins_with_semicolon() {
167        let mut deps = Deps::new();
168        deps.push("botan", PathBuf::from("/a/botan"));
169        deps.push("jsonc", PathBuf::from("/b/json-c"));
170        assert_eq!(deps.cmake_prefix_path(), "/a/botan;/b/json-c");
171    }
172
173    #[test]
174    fn empty_deps_yields_empty_prefix_path() {
175        let deps = Deps::new();
176        assert_eq!(deps.cmake_prefix_path(), "");
177    }
178
179    #[test]
180    fn deps_by_name_finds_pushed_entry() {
181        let mut deps = Deps::new();
182        deps.push("botan", PathBuf::from("/x/botan"));
183        assert_eq!(
184            deps.by_name("botan").map(|d| d.prefix.clone()),
185            Some(PathBuf::from("/x/botan"))
186        );
187        assert!(deps.by_name("jsonc").is_none());
188    }
189
190    #[test]
191    fn deps_push_rejects_unknown_name() {
192        let mut deps = Deps::new();
193        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
194            deps.push("nonexistent", PathBuf::from("/x"));
195        }));
196        assert!(result.is_err(), "push should panic for unknown dep name");
197    }
198
199    #[test]
200    fn cmake_dep_source_and_build_dirs() {
201        let dep = CmakeDep {
202            name: "json-c",
203            version: "0.17",
204            url_template: "",
205            extra_cmake_args: &[],
206            cmake_policy_minimum: None,
207        };
208        let root = Path::new("/tmp/src");
209        assert_eq!(dep.source_dir(root), PathBuf::from("/tmp/src/json-c-0.17"));
210        assert_eq!(dep.build_dir(root), PathBuf::from("/tmp/src/json-c-build"));
211    }
212
213    #[test]
214    fn cmake_dep_url_substitutes_version() {
215        assert_eq!(
216            JSON_C.url(),
217            "https://s3.amazonaws.com/json-c_releases/releases/json-c-0.17.tar.gz"
218        );
219        assert_eq!(
220            ZLIB.url(),
221            "https://github.com/madler/zlib/releases/download/v1.3.1/zlib-1.3.1.tar.gz"
222        );
223    }
224}