use std::path::{Path, PathBuf};
use super::wrapper::CapturedRustcInvocation;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjBuildPlan {
pub args: Vec<String>,
pub output_dir: PathBuf,
pub expected_object: PathBuf,
}
pub fn object_filename(crate_name: &str) -> String {
let stem = crate_name.replace('-', "_");
format!("{stem}.o")
}
pub fn build_obj_plan(captured: &CapturedRustcInvocation, output_dir: &Path) -> ObjBuildPlan {
let mut args = captured.args.clone();
set_crate_type(&mut args, "rlib");
set_out_dir(&mut args, output_dir);
let object_path = output_dir.join(object_filename(&captured.crate_name));
set_emit_obj(&mut args, &object_path);
strip_json_flags(&mut args);
set_incremental(&mut args, &output_dir.join("incremental"));
override_opt_level(&mut args, "0");
ObjBuildPlan {
args,
output_dir: output_dir.to_path_buf(),
expected_object: object_path,
}
}
fn override_opt_level(args: &mut Vec<String>, level: &str) {
let mut i = 0;
while i < args.len() {
if (args[i] == "-C" || args[i] == "--codegen")
&& i + 1 < args.len()
&& args[i + 1].starts_with("opt-level=")
{
args.drain(i..i + 2);
continue;
}
if args[i].starts_with("-Copt-level=") || args[i].starts_with("--codegen=opt-level=") {
args.remove(i);
continue;
}
if args[i] == "-O" {
args.remove(i);
continue;
}
i += 1;
}
args.push("-C".into());
args.push(format!("opt-level={level}"));
}
fn set_incremental(args: &mut Vec<String>, incremental_dir: &Path) {
let mut i = 0;
while i < args.len() {
if (args[i] == "-C" || args[i] == "--codegen")
&& i + 1 < args.len()
&& args[i + 1].starts_with("incremental=")
{
args.drain(i..=i + 1);
continue;
}
if args[i].starts_with("-Cincremental=") || args[i].starts_with("--codegen=incremental=") {
args.remove(i);
continue;
}
i += 1;
}
args.push("-C".into());
args.push(format!("incremental={}", incremental_dir.display()));
}
fn strip_json_flags(args: &mut Vec<String>) {
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if (arg == "--json" || arg == "--error-format") && i + 1 < args.len() {
args.drain(i..=i + 1);
continue;
}
if arg.starts_with("--json=") || arg.starts_with("--error-format=") {
args.remove(i);
continue;
}
i += 1;
}
}
pub fn set_emit_obj(args: &mut Vec<String>, object_path: &Path) {
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "--emit" && i + 1 < args.len() {
args.drain(i..=i + 1);
continue;
}
if arg.starts_with("--emit=") {
args.remove(i);
continue;
}
i += 1;
}
args.push("--emit".into());
args.push(format!("obj={}", object_path.to_string_lossy()));
}
pub fn set_crate_type(args: &mut Vec<String>, new_kind: &str) {
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if arg == "--crate-type" && i + 1 < args.len() {
args.drain(i..=i + 1);
continue;
}
if arg.starts_with("--crate-type=") {
args.remove(i);
continue;
}
i += 1;
}
args.push("--crate-type".into());
args.push(new_kind.into());
}
pub fn library_filename(crate_name: &str) -> String {
let stem = crate_name.replace('-', "_");
if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
format!("lib{stem}.dylib")
} else if cfg!(target_os = "windows") {
format!("{stem}.dll")
} else {
format!("lib{stem}.so")
}
}
pub fn library_filename_for_os(crate_name: &str, os: super::link_plan::LinkerOs) -> String {
use super::link_plan::LinkerOs;
let stem = crate_name.replace('-', "_");
match os {
LinkerOs::Macos => format!("lib{stem}.dylib"),
LinkerOs::Linux => format!("lib{stem}.so"),
LinkerOs::Other => format!("{stem}.dll"),
}
}
pub fn set_out_dir(args: &mut Vec<String>, dir: &Path) {
let dir_str = dir.to_string_lossy().to_string();
let mut i = 0;
while i < args.len() {
let arg = &args[i];
if (arg == "--out-dir" || arg == "-o") && i + 1 < args.len() {
args.drain(i..=i + 1);
continue;
}
if arg.starts_with("--out-dir=") {
args.remove(i);
continue;
}
i += 1;
}
args.push("--out-dir".into());
args.push(dir_str);
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
fn captured_with(args: Vec<String>) -> CapturedRustcInvocation {
CapturedRustcInvocation {
crate_name: "demo".into(),
args,
timestamp_micros: 0,
}
}
#[test]
fn set_crate_type_replaces_a_single_existing_separated_pair() {
let mut args = s(&["--edition=2021", "--crate-type", "rlib", "src/lib.rs"]);
set_crate_type(&mut args, "cdylib");
assert_eq!(
args,
s(&["--edition=2021", "src/lib.rs", "--crate-type", "cdylib"]),
);
}
#[test]
fn set_crate_type_replaces_the_equals_form() {
let mut args = s(&["--crate-type=rlib", "--edition=2021"]);
set_crate_type(&mut args, "cdylib");
assert_eq!(args, s(&["--edition=2021", "--crate-type", "cdylib"]));
}
#[test]
fn set_crate_type_collapses_multiple_existing_into_one() {
let mut args = s(&[
"--crate-type",
"rlib",
"--crate-type",
"dylib",
"--crate-type=staticlib",
"src/lib.rs",
]);
set_crate_type(&mut args, "cdylib");
assert_eq!(args, s(&["src/lib.rs", "--crate-type", "cdylib"]));
}
#[test]
fn set_crate_type_appends_when_no_existing() {
let mut args = s(&["--edition=2021", "src/lib.rs"]);
set_crate_type(&mut args, "cdylib");
assert_eq!(
args,
s(&["--edition=2021", "src/lib.rs", "--crate-type", "cdylib"]),
);
}
#[test]
fn set_out_dir_replaces_separated_form() {
let mut args = s(&["--out-dir", "/old/path", "src/lib.rs"]);
set_out_dir(&mut args, Path::new("/new/path"));
assert_eq!(args, s(&["src/lib.rs", "--out-dir", "/new/path"]));
}
#[test]
fn set_out_dir_replaces_equals_form() {
let mut args = s(&["--out-dir=/old/path", "src/lib.rs"]);
set_out_dir(&mut args, Path::new("/new/path"));
assert_eq!(args, s(&["src/lib.rs", "--out-dir", "/new/path"]));
}
#[test]
fn set_out_dir_replaces_the_short_o_form() {
let mut args = s(&["-o", "/old/file.rlib", "src/lib.rs"]);
set_out_dir(&mut args, Path::new("/new/path"));
assert_eq!(args, s(&["src/lib.rs", "--out-dir", "/new/path"]));
}
#[test]
fn set_out_dir_appends_when_no_existing() {
let mut args = s(&["src/lib.rs"]);
set_out_dir(&mut args, Path::new("/new/path"));
assert_eq!(args, s(&["src/lib.rs", "--out-dir", "/new/path"]));
}
#[test]
fn set_emit_obj_replaces_separated_form() {
let mut args = s(&["--emit", "link", "src/lib.rs"]);
set_emit_obj(&mut args, Path::new("/p/demo.o"));
assert_eq!(args, s(&["src/lib.rs", "--emit", "obj=/p/demo.o"]));
}
#[test]
fn set_emit_obj_replaces_equals_form_including_comma_lists() {
let mut args = s(&["--emit=dep-info,metadata,link", "src/lib.rs"]);
set_emit_obj(&mut args, Path::new("/p/demo.o"));
assert_eq!(args, s(&["src/lib.rs", "--emit", "obj=/p/demo.o"]));
}
#[test]
fn set_emit_obj_collapses_multiple_existing_into_one() {
let mut args = s(&[
"--emit",
"link",
"--emit=dep-info,metadata",
"--emit",
"metadata",
"src/lib.rs",
]);
set_emit_obj(&mut args, Path::new("/p/demo.o"));
assert_eq!(args, s(&["src/lib.rs", "--emit", "obj=/p/demo.o"]));
}
#[test]
fn set_emit_obj_appends_when_no_existing() {
let mut args = s(&["src/lib.rs"]);
set_emit_obj(&mut args, Path::new("/p/demo.o"));
assert_eq!(args, s(&["src/lib.rs", "--emit", "obj=/p/demo.o"]));
}
#[test]
fn object_filename_is_crate_dot_o_with_underscores() {
assert_eq!(object_filename("demo"), "demo.o");
assert_eq!(object_filename("hello-world"), "hello_world.o");
assert_eq!(object_filename("a-b-c"), "a_b_c.o");
}
#[test]
fn obj_plan_forces_rlib_and_obj_emit_and_redirects_out_dir() {
let captured = captured_with(s(&[
"--edition=2021",
"--crate-name",
"demo",
"--crate-type",
"lib",
"--emit=dep-info,metadata,link",
"--out-dir",
"/cargo/target/debug/deps",
"-C",
"opt-level=3",
"src/lib.rs",
]));
let plan = build_obj_plan(&captured, Path::new("/whisker/objs/x"));
assert_eq!(
plan.args,
s(&[
"--edition=2021",
"--crate-name",
"demo",
"src/lib.rs",
"--crate-type",
"rlib",
"--out-dir",
"/whisker/objs/x",
"--emit",
"obj=/whisker/objs/x/demo.o",
"-C",
"incremental=/whisker/objs/x/incremental",
"-C",
"opt-level=0",
]),
);
assert_eq!(plan.output_dir, Path::new("/whisker/objs/x"));
assert_eq!(plan.expected_object, Path::new("/whisker/objs/x/demo.o"));
}
#[test]
fn obj_plan_picks_object_filename_from_captured_crate_name() {
let captured = CapturedRustcInvocation {
crate_name: "thin-build-fixture".into(),
args: s(&["src/lib.rs"]),
timestamp_micros: 0,
};
let plan = build_obj_plan(&captured, Path::new("/o"));
assert_eq!(plan.expected_object, Path::new("/o/thin_build_fixture.o"));
assert!(
plan.args.contains(&"obj=/o/thin_build_fixture.o".into()),
"args: {:?}",
plan.args,
);
}
#[test]
fn obj_plan_is_idempotent_on_re_run() {
let captured = captured_with(s(&["src/lib.rs"]));
let plan1 = build_obj_plan(&captured, Path::new("/o"));
let plan2 = build_obj_plan(
&CapturedRustcInvocation {
crate_name: captured.crate_name.clone(),
args: plan1.args.clone(),
timestamp_micros: 0,
},
Path::new("/o"),
);
assert_eq!(plan1.args, plan2.args);
}
#[test]
fn obj_plan_preserves_target_triple_and_sysroot_args() {
let captured = captured_with(s(&[
"--target",
"aarch64-linux-android",
"--sysroot",
"/some/ndk/sysroot",
"-Clinker=lld",
"-Clink-arg=-fuse-ld=lld",
"-L",
"native=/some/lib",
"-l",
"log",
"src/lib.rs",
]));
let plan = build_obj_plan(&captured, Path::new("/o"));
for needle in [
"--target",
"aarch64-linux-android",
"--sysroot",
"/some/ndk/sysroot",
"-Clinker=lld",
"-Clink-arg=-fuse-ld=lld",
"-L",
"native=/some/lib",
"-l",
"log",
] {
assert!(
plan.args.iter().any(|a| a == needle),
"missing {needle:?} from {:?}",
plan.args,
);
}
}
}