Skip to content

Commit 721e13e

Browse files
committed
ctest: Add translation of Rust types.
1 parent 4241857 commit 721e13e

23 files changed

+902
-417
lines changed

ctest-next/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ askama = "0.14.0"
1212
cc = "1.2.25"
1313
quote = "1.0.40"
1414
syn = { version = "2.0.101", features = ["full", "visit", "extra-traits"] }
15+
thiserror = "2.0.12"
16+
17+
[dev-dependencies]
1518
tempfile = "3.20.0"
19+

ctest-next/build.rs

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,55 @@
11
use std::env;
22

3+
// When we call `cargo test` for a cross compiled target, the following is required:
4+
// - CARGO_TARGET_{}_LINKER: To link the integration tests.
5+
// - CARGO_TARGET_{}_RUNNER: To run the integration tests.
6+
//
7+
// This is already set by the CI for all platforms, so there is no problem up till here.
8+
//
9+
// The integration tests (which are run in qemu, but use host rustc and cc) require the following:
10+
// - TARGET_PLATFORM or target set manually. (We forward TARGET in build.rs for this.)
11+
// - HOST_PLATFORM or host set manually. (We forward HOST in build.rs for this.)
12+
// - LINKER: To link the C headers. (We forward CARGO_TARGET_{}_LINKER for this.)
13+
// - FLAGS: Any flags to pass when compiling the test binary for the cross compiled platform. (Forwarded from CARGO_TARGET_{}_RUSTFLAGS)
14+
// - RUNNER: To run the test binary with. (Forward the same runner as CARGO_TARGET_{}_RUNNER)
15+
//
16+
// The TARGET_PLATFORM and HOST_PLATFORM variables are not an issue, cargo will
17+
// automatically set TARGET and PLATFORM and we will forward them.
18+
//
19+
// Similarly FLAGS and RUNNER are also not an issue, if CARGO_TARGET_{}_RUSTFLAGS are present they're forwarded.
20+
// And RUSTFLAGS works by default anyway. Similarly the test binary doesn't require any external applications so just
21+
// the RUNNER is enough to run it.
22+
//
23+
// However since rustc and cc are the host versions, they will only work if we specify the correct variables for them.
24+
// Because we only use them to compile, not run things.
25+
// For CC we MUST specify CC or CC_target otherwise it will fail. (Other flags like AR etc. work without forwarding because it is run in the host.)
26+
// For rustc we MUST specify the correct linker. Usually this is the same as CC or CC_target.
27+
//
28+
// In the CI, the CARGO_TARGET_{} variables are always set.
29+
330
fn main() {
431
let host = env::var("HOST").unwrap();
532
let target = env::var("TARGET").unwrap();
6-
let target_key = target.replace('-', "_");
33+
let target_key = target.replace('-', "_").to_uppercase();
734

835
println!("cargo:rustc-env=HOST_PLATFORM={host}");
936
println!("cargo:rustc-env=TARGET_PLATFORM={target}");
1037

11-
let linker = env::var(format!("CARGO_TARGET_{}_LINKER", target_key.to_uppercase()))
12-
.or_else(|_| env::var("CC"))
13-
.or_else(|_| env::var(format!("CC_{target_key}")))
14-
.unwrap_or_default();
15-
16-
let runner =
17-
env::var(format!("CARGO_TARGET_{}_RUNNER", target_key.to_uppercase())).unwrap_or_default();
18-
38+
let linker = env::var(format!("CARGO_TARGET_{target_key}_LINKER")).unwrap_or_default();
39+
let runner = env::var(format!("CARGO_TARGET_{target_key}_RUNNER")).unwrap_or_default();
1940
// As we invoke rustc directly this does not get passed to it, although RUSTFLAGS does.
20-
let flags = env::var(format!(
21-
"CARGO_TARGET_{}_RUSTFLAGS",
22-
target_key.to_uppercase()
23-
))
24-
.unwrap_or_default();
41+
let flags = env::var(format!("CARGO_TARGET_{target_key}_RUSTFLAGS")).unwrap_or_default();
2542

43+
// Forward build environment variables so they can be used in tests.
2644
println!("cargo:rustc-env=LINKER={linker}");
2745
println!("cargo:rustc-env=RUNNER={runner}");
2846
println!("cargo:rustc-env=FLAGS={flags}");
2947

48+
// Rerun this build script if any of the environment variables change.
49+
println!("cargo:rerun-if-changed-env=HOST");
3050
println!("cargo:rerun-if-changed-env=TARGET");
51+
println!("cargo:rerun-if-changed-env=CARGO_TARGET_{target_key}_LINKER",);
52+
println!("cargo:rerun-if-changed-env=CC");
53+
println!("cargo:rerun-if-changed-env=CC_{target_key}");
54+
println!("cargo:rerun-if-changed-env=CARGO_TARGET_{target_key}_RUNNER",);
3155
}

ctest-next/src/generator.rs

Lines changed: 15 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,12 @@ use crate::{
1616
};
1717

1818
/// A builder used to generate a test suite.
19-
#[non_exhaustive]
2019
#[derive(Default, Debug, Clone)]
2120
pub struct TestGenerator {
2221
headers: Vec<String>,
23-
target: Option<String>,
24-
host: Option<String>,
25-
includes: Vec<PathBuf>,
22+
pub(crate) target: Option<String>,
23+
pub(crate) host: Option<String>,
24+
pub(crate) includes: Vec<PathBuf>,
2625
out_dir: Option<PathBuf>,
2726
}
2827

@@ -33,12 +32,19 @@ impl TestGenerator {
3332
}
3433

3534
/// Add a header to be included as part of the generated C file.
35+
///
36+
/// The generate C test will be compiled by a C compiler, and this can be
37+
/// used to ensure that all the necessary header files are included to test
38+
/// all FFI definitions.
3639
pub fn header(&mut self, header: &str) -> &mut Self {
3740
self.headers.push(header.to_string());
3841
self
3942
}
4043

4144
/// Configures the target to compile C code for.
45+
///
46+
/// Note that for Cargo builds this defaults to `$TARGET` and it's not
47+
/// necessary to call.
4248
pub fn target(&mut self, target: &str) -> &mut Self {
4349
self.target = Some(target.to_string());
4450
self
@@ -65,71 +71,13 @@ impl TestGenerator {
6571
self
6672
}
6773

68-
/// Generate all tests for the given crate and output the Rust side to a file.
69-
pub fn generate<P: AsRef<Path>>(&mut self, crate_path: P, output_file_path: P) -> Result<()> {
70-
let output_file_path = self.generate_files(crate_path, output_file_path)?;
71-
72-
let target = self
73-
.target
74-
.clone()
75-
.unwrap_or(env::var("TARGET_PLATFORM").unwrap());
76-
let host = self
77-
.host
78-
.clone()
79-
.unwrap_or(env::var("HOST_PLATFORM").unwrap());
80-
81-
let mut cfg = cc::Build::new();
82-
// FIXME: Cpp not supported.
83-
cfg.file(output_file_path.with_extension("c"));
84-
cfg.host(&host);
85-
if target.contains("msvc") {
86-
cfg.flag("/W3")
87-
.flag("/Wall")
88-
.flag("/WX")
89-
// ignored warnings
90-
.flag("/wd4820") // warning about adding padding?
91-
.flag("/wd4100") // unused parameters
92-
.flag("/wd4996") // deprecated functions
93-
.flag("/wd4296") // '<' being always false
94-
.flag("/wd4255") // converting () to (void)
95-
.flag("/wd4668") // using an undefined thing in preprocessor?
96-
.flag("/wd4366") // taking ref to packed struct field might be unaligned
97-
.flag("/wd4189") // local variable initialized but not referenced
98-
.flag("/wd4710") // function not inlined
99-
.flag("/wd5045") // compiler will insert Spectre mitigation
100-
.flag("/wd4514") // unreferenced inline function removed
101-
.flag("/wd4711"); // function selected for automatic inline
102-
} else {
103-
cfg.flag("-Wall")
104-
.flag("-Wextra")
105-
.flag("-Werror")
106-
.flag("-Wno-unused-parameter")
107-
.flag("-Wno-type-limits")
108-
// allow taking address of packed struct members:
109-
.flag("-Wno-address-of-packed-member")
110-
.flag("-Wno-unknown-warning-option")
111-
.flag("-Wno-deprecated-declarations"); // allow deprecated items
112-
}
113-
114-
for p in &self.includes {
115-
cfg.include(p);
116-
}
117-
118-
let stem: &str = output_file_path.file_stem().unwrap().to_str().unwrap();
119-
cfg.target(&target)
120-
.out_dir(output_file_path.parent().unwrap())
121-
.compile(stem);
122-
123-
Ok(())
124-
}
125-
12674
/// Generate the Rust and C testing files.
12775
///
128-
/// Returns the path to the generated file.
129-
pub(crate) fn generate_files<P: AsRef<Path>>(
76+
/// Returns the path to t generated file.
77+
pub fn generate_files(
13078
&mut self,
131-
crate_path: P,
132-
output_file_path: P,
79+
crate_path: impl AsRef<Path>,
80+
output_file_path: impl AsRef<Path>,
13381
) -> Result<PathBuf> {
13482
let expanded = expand(crate_path)?;
13583
let ast = syn::parse_file(&expanded)?;
@@ -140,7 +88,7 @@ impl TestGenerator {
14088
let output_directory = self
14189
.out_dir
14290
.clone()
143-
.unwrap_or_else(|| PathBuf::from(env::var_os("OUT_DIR").unwrap()));
91+
.unwrap_or_else(|| env::var("OUT_DIR").unwrap().into());
14492
let output_file_path = output_directory.join(output_file_path);
14593

14694
// Generate the Rust side of the tests.

ctest-next/src/lib.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,14 @@ mod ffi_items;
1616
mod generator;
1717
mod macro_expansion;
1818
mod runner;
19-
mod rustc_version;
2019
mod template;
2120
mod translator;
2221

2322
pub use ast::{Abi, Const, Field, Fn, Parameter, Static, Struct, Type, Union};
2423
pub use generator::TestGenerator;
2524
pub use macro_expansion::expand;
26-
pub use runner::{compile_test, run_test};
27-
pub use rustc_version::{rustc_version, RustcVersion};
25+
pub use runner::{__compile_test, __run_test, generate_test};
26+
pub use translator::TranslationError;
2827

2928
/// A possible error that can be encountered in our library.
3029
pub type Error = Box<dyn std::error::Error>;

ctest-next/src/macro_expansion.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ pub fn expand<P: AsRef<Path>>(crate_path: P) -> Result<String> {
99
let output = Command::new(rustc)
1010
.env("RUSTC_BOOTSTRAP", "1")
1111
.arg("-Zunpretty=expanded")
12+
.arg("--edition")
13+
.arg("2024") // By default, -Zunpretty=expanded uses 2015 edition.
1214
.arg(canonicalize(crate_path)?)
1315
.output()?;
1416

ctest-next/src/runner.rs

Lines changed: 93 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,124 @@
1-
use crate::Result;
1+
use crate::{Result, TestGenerator};
22
use std::env;
33
use std::fs::{canonicalize, File};
44
use std::io::Write;
55
use std::path::{Path, PathBuf};
66
use std::process::Command;
77

8+
/// Generate all tests for the given crate and output the Rust side to a file.
9+
pub fn generate_test(
10+
generator: &mut TestGenerator,
11+
crate_path: impl AsRef<Path>,
12+
output_file_path: impl AsRef<Path>,
13+
) -> Result<PathBuf> {
14+
let output_file_path = generator.generate_files(crate_path, output_file_path)?;
15+
16+
// Search for the target and host to build for if specified manually (generator.target, generator.host),
17+
// via build script (TARGET, HOST), or for internal testing (TARGET_PLATFORM, HOST_PLATFORM).
18+
let target = generator.target.clone().unwrap_or_else(|| {
19+
env::var("TARGET").unwrap_or_else(|_| env::var("TARGET_PLATFORM").unwrap())
20+
});
21+
let host = generator
22+
.host
23+
.clone()
24+
.unwrap_or_else(|| env::var("HOST").unwrap_or_else(|_| env::var("HOST_PLATFORM").unwrap()));
25+
26+
let mut cfg = cc::Build::new();
27+
// FIXME: Cpp not supported.
28+
cfg.file(output_file_path.with_extension("c"));
29+
cfg.host(&host);
30+
31+
if target.contains("msvc") {
32+
cfg.flag("/W3")
33+
.flag("/Wall")
34+
.flag("/WX")
35+
// ignored warnings
36+
.flag("/wd4820") // warning about adding padding?
37+
.flag("/wd4100") // unused parameters
38+
.flag("/wd4996") // deprecated functions
39+
.flag("/wd4296") // '<' being always false
40+
.flag("/wd4255") // converting () to (void)
41+
.flag("/wd4668") // using an undefined thing in preprocessor?
42+
.flag("/wd4366") // taking ref to packed struct field might be unaligned
43+
.flag("/wd4189") // local variable initialized but not referenced
44+
.flag("/wd4710") // function not inlined
45+
.flag("/wd5045") // compiler will insert Spectre mitigation
46+
.flag("/wd4514") // unreferenced inline function removed
47+
.flag("/wd4711"); // function selected for automatic inline
48+
} else {
49+
cfg.flag("-Wall")
50+
.flag("-Wextra")
51+
.flag("-Werror")
52+
.flag("-Wno-unused-parameter")
53+
.flag("-Wno-type-limits")
54+
// allow taking address of packed struct members:
55+
.flag("-Wno-address-of-packed-member")
56+
.flag("-Wno-unknown-warning-option")
57+
.flag("-Wno-deprecated-declarations"); // allow deprecated items
58+
}
59+
60+
for p in &generator.includes {
61+
cfg.include(p);
62+
}
63+
64+
let stem: &str = output_file_path.file_stem().unwrap().to_str().unwrap();
65+
cfg.target(&target)
66+
.out_dir(output_file_path.parent().unwrap())
67+
.compile(stem);
68+
69+
Ok(output_file_path)
70+
}
71+
872
/// Compiles a Rust source file and links it against a static library.
973
///
1074
/// Returns the path to the generated binary.
11-
pub fn compile_test<P: AsRef<Path>>(
12-
output_dir: P,
13-
crate_path: P,
14-
library_file: P,
75+
#[doc(hidden)]
76+
pub fn __compile_test(
77+
output_dir: impl AsRef<Path>,
78+
crate_path: impl AsRef<Path>,
79+
library_file: impl AsRef<Path>,
1580
) -> Result<PathBuf> {
1681
let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".into());
1782
let output_dir = output_dir.as_ref();
1883
let crate_path = crate_path.as_ref();
19-
let library_file = library_file.as_ref();
84+
let library_file = library_file.as_ref().file_stem().unwrap();
2085

2186
let rust_file = output_dir
2287
.join(crate_path.file_stem().unwrap())
2388
.with_extension("rs");
2489
let binary_path = output_dir.join(rust_file.file_stem().unwrap());
2590

91+
// Create a file that contains the Rust 'bindings' as well as the generated test code.
2692
File::create(&rust_file)?.write_all(
2793
format!(
2894
"include!(r#\"{}\"#);\ninclude!(r#\"{}.rs\"#);",
2995
canonicalize(crate_path)?.display(),
30-
library_file.display()
96+
library_file.to_str().unwrap()
3197
)
3298
.as_bytes(),
3399
)?;
34100

101+
// Compile the test file with the compiled C library file found in `output_dir`
102+
// into a binary file, ignoring all warnings about unused items. (not all items
103+
// are currently tested)
104+
35105
let mut cmd = Command::new(rustc);
36106
cmd.arg(&rust_file)
37107
.arg(format!("-Lnative={}", output_dir.display()))
38-
.arg(format!(
39-
"-lstatic={}",
40-
library_file.file_stem().unwrap().to_str().unwrap()
41-
))
42-
.arg("--target")
43-
.arg(env::var("TARGET_PLATFORM").unwrap())
108+
.arg(format!("-lstatic={}", library_file.to_str().unwrap()))
109+
.arg("--edition")
110+
.arg("2021") // Defaults to 2015.
44111
.arg("-o")
45112
.arg(&binary_path)
46113
.arg("-Aunused");
47114

115+
// Pass in a different target, linker or flags if set, useful for cross compilation.
116+
117+
let target = env::var("TARGET_PLATFORM").unwrap_or_default();
118+
if !target.is_empty() {
119+
cmd.arg("--target").arg(target);
120+
}
121+
48122
let linker = env::var("LINKER").unwrap_or_default();
49123
if !linker.is_empty() {
50124
cmd.arg(format!("-Clinker={linker}"));
@@ -64,14 +138,17 @@ pub fn compile_test<P: AsRef<Path>>(
64138
}
65139

66140
/// Executes the compiled test binary and returns its output.
67-
pub fn run_test(test_binary: &str) -> Result<String> {
141+
///
142+
/// If a RUNNER environment variable is present, it will use that to run the binary.
143+
#[doc(hidden)]
144+
pub fn __run_test<P: AsRef<Path>>(test_binary: P) -> Result<String> {
68145
let runner = env::var("RUNNER").unwrap_or_default();
69146
let output = if runner.is_empty() {
70-
Command::new(test_binary).output()?
147+
Command::new(test_binary.as_ref()).output()?
71148
} else {
72149
let mut args = runner.split_whitespace();
73150
let mut cmd = Command::new(args.next().unwrap());
74-
cmd.args(args).arg(test_binary).output()?
151+
cmd.args(args).arg(test_binary.as_ref()).output()?
75152
};
76153

77154
if !output.status.success() {

0 commit comments

Comments
 (0)