-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathbuild.rs
372 lines (320 loc) · 15 KB
/
build.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
#![allow(clippy::panic)] // Panic only during build time.
use std::{
env,
fs::File,
io::{self, Write as _},
path::{Path, PathBuf},
};
/// Target path in CMake build for include files.
const CMAKE_INCLUDE: &str = "include";
/// Target path in CMake build for lib files.
const CMAKE_LIB: &str = "lib";
/// Name of target library from `open62541` build. This must be `open62541` as it is being generated
/// by the CMake build.
const LIB_BASE: &str = "open62541";
/// Name of library from `extern.c` and `wrapper.c` that holds additional helpers, in particular the
/// compilation of static (inline) functions from `open62541` itself. This may be an arbitrary name;
/// the `cc` build adds it as `rustc-link-lib` automatically.
const LIB_EXT: &str = "open62541-ext";
/// Pattern to search for compatibility with Edition 2024.
///
/// See also [`LEGACY_EXTERN_REPLACEMENT`].
const LEGACY_EXTERN_PATTERN: &str = r#"extern "C" {"#;
/// Replacement to use for compatibility with Edition 2024.
///
/// See also [`LEGACY_EXTERN_PATTERN`].
const LEGACY_EXTERN_REPLACEMENT: &str = r#"unsafe extern "C" {"#;
fn main() {
let with_mbedtls =
matches!(env::var("CARGO_FEATURE_MBEDTLS"), Ok(mbedtls) if !mbedtls.is_empty());
let with_openssl =
matches!(env::var("CARGO_FEATURE_OPENSSL"), Ok(openssl) if !openssl.is_empty());
// For now, we do not actually announce feature flag `openssl` in `Cargo.toml`.
let encryption = match (with_mbedtls, with_openssl) {
(false, false) => None,
(true, false) => Some(Encryption::MbedTls),
(false, true) => Some(Encryption::OpenSsl),
_ => panic!("conflicting encryption feature flags, only one must be enabled"),
};
let src = env::current_dir().expect("should get current directory");
// Get derived paths relative to `src`.
let src_mbedtls = src.join("mbedtls");
let src_open62541 = src.join("open62541");
let src_wrapper_c = src.join("wrapper.c");
let src_wrapper_h = src.join("wrapper.h");
// Rerun build when files in `src` change.
println!("cargo:rerun-if-changed={}", src_open62541.display());
println!("cargo:rerun-if-changed={}", src_wrapper_c.display());
println!("cargo:rerun-if-changed={}", src_wrapper_h.display());
// Build related encryption libraries.
let encryption_dst = encryption.map(|encryption| match encryption {
Encryption::MbedTls => prepare_mbedtls(src_mbedtls),
Encryption::OpenSsl => prepare_openssl(),
});
// Build `open62541` library.
let dst = build_open62541(src_open62541, encryption_dst.as_ref());
// Get derived paths relative to `dst`.
let dst_include = dst.join(CMAKE_INCLUDE);
let dst_lib = dst.join(CMAKE_LIB);
if matches!(env::var("CARGO_CFG_TARGET_OS"), Ok(os) if os == "windows") {
// We require the `Iphlpapi` library on Windows builds to avoid errors (regarding the use of
// `if_nametoindex`, see https://github.com/open62541/open62541/issues/5622).
println!("cargo:rustc-link-lib=Iphlpapi");
}
println!("cargo:rustc-link-search={}", dst_lib.display());
println!("cargo:rustc-link-lib={LIB_BASE}");
// For encryption support enabled, we add the libraries that have to be used as dependencies for
// the final build artifact.
//
// Note: These must come _after_ adding `LIB_BASE` above for linker to resolve dependencies.
if let Some(encryption_dst) = encryption_dst {
encryption_dst.rustc_link_search();
encryption_dst.rustc_link_lib();
}
let out = PathBuf::from(env::var("OUT_DIR").expect("should have OUT_DIR"));
// Get derived paths relative to `out`.
let out_bindings_rs = out.join("bindings.rs");
let out_extern_c = out.join("extern.c");
let builder = bindgen::Builder::default()
// Include our wrapper functions.
.allowlist_function("(__)?RS_.*")
.allowlist_function("(__)?UA_.*")
// Include our wrapper types.
.allowlist_type("(__)?RS_.*")
.allowlist_type("(__)?UA_.*")
// Include our wrapper vars.
.allowlist_var("(__)?RS_.*")
.allowlist_var("(__)?UA_.*")
// Explicitly set C99 standard to force Windows variants of `vsnprintf()` to conform to this
// standard. This also matches the expected (or supported) C standard of `open62541` itself.
.clang_arg("-std=c99")
.clang_arg(format!("-I{}", dst_include.display()))
.default_enum_style(bindgen::EnumVariation::NewType {
is_bitfield: false,
is_global: false,
})
// Use explicit Rust target version that matches the entry in `Cargo.toml`.
.rust_target(
bindgen::RustTarget::stable(72, 0)
.ok()
.expect("should be a valid Rust target"),
)
// Do not derive `Copy` because most of the data types are not copy-safe (they own memory by
// pointers and need to be cloned manually to duplicate that memory).
.derive_copy(false)
// We want to initialize some types statically. This is used in `open62541`, we require that
// as well to mirror some of the functionality.
.derive_default(true)
// The auto-derived comments are not particularly useful because they often do not match the
// declaration they belong to.
.generate_comments(false)
.header(src_wrapper_h.to_str().expect("should be valid path"))
// Activate parse callbacks. This causes cargo to invalidate the generated bindings when any
// of the included files change. It also enables us to rename items in the final bindings.
.parse_callbacks(Box::new(CustomCallbacks { dst }))
// We may use `core` instead of `std`. This might be useful for `no_std` environments.
.use_core()
// Wrap static functions. These are used in several places for inline helpers and we want to
// preserve those in the generated bindings. This outputs `extern.c` which we compile below.
.wrap_static_fns(true)
// Make sure to specify the location of the resulting `extern.c`. By default `bindgen` would
// place it in the temporary directory.
.wrap_static_fns_path(out_extern_c.to_str().expect("should be valid path"));
let bindings = builder
.generate()
.expect("should generate `Bindings` instance");
bindings
.write_to_file(out_bindings_rs.clone())
.expect("should write `bindings.rs`");
// Until <https://github.com/rust-lang/rust-bindgen/issues/2901> is resolved, we replace `extern
// "C"` with `unsafe extern "C"` manually here. Remove this when `bindgen` is able to do it.
if version_check::is_min_version("1.82.0") == Some(true) {
// We can only use `unsafe extern` starting with Rust 1.82.0. See
// <https://blog.rust-lang.org/2024/10/17/Rust-1.82.0.html#safe-items-with-unsafe-extern>.
replace_in_file(
&out_bindings_rs,
LEGACY_EXTERN_PATTERN,
LEGACY_EXTERN_REPLACEMENT,
)
.expect("should add unsafe to extern statements");
}
// Build `extern.c` and our custom `wrapper.c` that both hold additional helpers that we want to
// link in addition to the base `open62541` library.
cc::Build::new()
.file(out_extern_c)
.file(src_wrapper_c)
.include(dst_include)
// Disable warnings for `open62541`. Not much we can do anyway.
.warnings(false)
// Explicitly disable deprecation warnings (seem to be enabled even when other warnings have
// been disabled above).
.flag_if_supported("-Wno-deprecated-declarations")
.flag_if_supported("-Wno-deprecated")
.compile(LIB_EXT);
}
#[derive(Debug)]
enum Encryption {
MbedTls,
OpenSsl,
}
#[derive(Debug)]
enum EncryptionDst {
MbedTls {
dst: PathBuf,
libs: Vec<&'static str>,
},
OpenSsl {
search: Option<&'static str>,
libs: Vec<&'static str>,
},
}
impl EncryptionDst {
const fn search(&self) -> Option<&'static str> {
match self {
EncryptionDst::MbedTls { .. } => None,
EncryptionDst::OpenSsl { search, .. } => *search,
}
}
fn libs(&self) -> &[&'static str] {
match self {
EncryptionDst::MbedTls { libs, .. } | EncryptionDst::OpenSsl { libs, .. } => libs,
}
}
fn rustc_link_search(&self) {
if let Some(search) = self.search() {
println!("cargo:rustc-link-search={search}");
}
}
fn rustc_link_lib(&self) {
for lib in self.libs() {
println!("cargo:rustc-link-lib={lib}");
}
}
}
fn prepare_mbedtls(src: PathBuf) -> EncryptionDst {
// Build bundled copy of `mbedtls` with CMake.
let mut cmake = cmake::Config::new(src);
cmake
// Use explicit paths here to avoid generating files where we do not expect them below.
.define("CMAKE_INSTALL_INCLUDEDIR", CMAKE_INCLUDE)
// Some systems (Fedora) default to `lib64/` instead of `lib/` for 64-bit libraries.
.define("CMAKE_INSTALL_LIBDIR", CMAKE_LIB)
// Use same C99 standard as is used for building `open62541`.
.define("C_STANDARD", "99")
// Skip building binary programs unnecessary for linking library.
.define("ENABLE_PROGRAMS", "OFF")
// Skip building test programs that we are not going to run anyway.
.define("ENABLE_TESTING", "OFF");
if matches!(env::var("CARGO_CFG_TARGET_OS"), Ok(os) if os == "macos") {
// Disable warnings as errors because macOS builds use `-mmacosx-version-min` and `--target`
// options that sometimes cause a warning that would prevent the build.
cmake.define("MBEDTLS_FATAL_WARNINGS", "OFF");
}
let dst = cmake.build();
// The set of MbedTLS libraries that must be linked to work with `open62541` has been taken from
// <https://github.com/open62541/open62541/blob/master/tools/cmake/FindMbedTLS.cmake>.
let mut libs = vec!["mbedtls", "mbedx509", "mbedcrypto"];
if matches!(env::var("CARGO_CFG_TARGET_OS"), Ok(os) if os == "windows") {
// For some reason, newer Rust versions (?) require an explicit import of `bcrypt.lib` while
// older Rust versions (?) seem fine without this import. Add it regardless.
libs.push("bcrypt");
}
EncryptionDst::MbedTls { dst, libs }
}
fn prepare_openssl() -> EncryptionDst {
// For macOS, we require the precise link path because we expect OpenSSL to be provided by using
// Homebrew.
let search = matches!(env::var("CARGO_CFG_TARGET_OS"), Ok(os) if os == "macos")
.then_some("/opt/homebrew/opt/openssl/lib");
let libs = vec!["ssl", "crypto"];
EncryptionDst::OpenSsl { search, libs }
}
fn build_open62541(src: PathBuf, encryption: Option<&EncryptionDst>) -> PathBuf {
// Build bundled copy of `open62541` with CMake.
let mut cmake = cmake::Config::new(src);
cmake
// Use explicit paths here to avoid generating files where we do not expect them below.
.define("CMAKE_INSTALL_INCLUDEDIR", CMAKE_INCLUDE)
// Some systems (Fedora) default to `lib64/` instead of `lib/` for 64-bit libraries.
.define("CMAKE_INSTALL_LIBDIR", CMAKE_LIB)
// Explicitly set C99 standard to force Windows variants of `vsnprintf()` to conform to this
// standard. This also matches the expected (or supported) C standard of `open62541` itself.
.define("C_STANDARD", "99")
// Python defaults to creating bytecode in `__pycache__` directories. During build, this may
// happen when the tool `nodeset_compiler` is called. When we package a crate, builds should
// never modify files outside of `OUT_DIR`, so we disable the cache to prevent this.
.env("PYTHONDONTWRITEBYTECODE", "1");
if matches!(env::var("CARGO_CFG_TARGET_ENV"), Ok(env) if env == "musl") {
let arch = env::var("CARGO_CFG_TARGET_ARCH").expect("should have CARGO_CFG_TARGET_ARCH");
// We require includes from the Linux headers which are not provided automatically when musl
// is targeted (see https://github.com/open62541/open62541/issues/6360).
// TODO: Remove this when `open62541` enables us to build without including Linux headers.
cmake
.cflag("-idirafter/usr/include")
.cflag(format!("-idirafter/usr/include/{arch}-linux-gnu"));
}
// When enabled, we build `open62541` with encryption support. This changes the library and also
// changes the resulting `bindings.rs`.
let encryption = match encryption {
None => "OFF",
Some(EncryptionDst::MbedTls { dst, .. }) => {
// Skip auto-detection and use explicit folders from `mbedtls` build.
cmake
.define("MBEDTLS_FOLDER_INCLUDE", dst.join(CMAKE_INCLUDE))
.define("MBEDTLS_FOLDER_LIBRARY", dst.join(CMAKE_LIB));
"MBEDTLS"
}
Some(EncryptionDst::OpenSsl { .. }) => "OPENSSL",
};
cmake.define("UA_ENABLE_ENCRYPTION", encryption);
cmake.build()
}
#[derive(Debug)]
struct CustomCallbacks {
/// Destination of CMake build of `open62541`.
dst: PathBuf,
}
impl CustomCallbacks {
/// Checks if `filename` is inside CMake destination.
///
/// This may be used to ensure that we do not run a rebuild when files generated by CMake change
/// (it is not necessary to include those files because we already watch the CMake _sources_ and
/// trigger a rebuild when they change).
fn inside_dst(&self, filename: &str) -> bool {
Path::new(filename).starts_with(&self.dst)
}
}
// Include `cargo:rerun-if` instructions just like `bindgen::CargoCallbacks` does. In addition, make
// necessary adjustments to the names of items for the final bindings.
impl bindgen::callbacks::ParseCallbacks for CustomCallbacks {
fn header_file(&self, filename: &str) {
// Make sure to rerun build when dependency outside of `dst/` changes.
if !self.inside_dst(filename) {
println!("cargo:rerun-if-changed={filename}");
}
}
fn include_file(&self, filename: &str) {
// Make sure to rerun build when dependency outside of `dst/` changes.
if !self.inside_dst(filename) {
println!("cargo:rerun-if-changed={filename}");
}
}
fn read_env_var(&self, key: &str) {
// Make sure to rerun build when environment variable changes.
println!("cargo:rerun-if-env-changed={key}");
}
fn item_name(&self, original_item_name: &str) -> Option<String> {
// Rename our wrapped custom exports to their intended names.
original_item_name.strip_prefix("RS_").map(str::to_owned)
}
}
/// Replaces all occurrences of pattern in file.
///
/// Note that this is not particularly efficient because it reads the entire file into memory before
/// writing it back. Care should be taken when operating on large files.
fn replace_in_file(path: &Path, pattern: &str, replacement: &str) -> io::Result<()> {
let buf = io::read_to_string(File::open(path)?)?;
let buf = buf.replace(pattern, replacement);
File::create(path)?.write_all(buf.as_bytes())?;
Ok(())
}