use serde::{Deserialize, Serialize};
use crate::constants::{DARWIN_SECTION_NAME, DARWIN_SEGMENT_NAME, ELF_SECTION_NAME};
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LtoMode {
#[default]
Marker,
SaveTemps,
Skip,
}
impl std::fmt::Display for LtoMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LtoMode::Marker => write!(f, "marker"),
LtoMode::SaveTemps => write!(f, "save-temps"),
LtoMode::Skip => write!(f, "skip"),
}
}
}
impl std::str::FromStr for LtoMode {
type Err = Error;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"marker" => Ok(Self::Marker),
"save-temps" => Ok(Self::SaveTemps),
"skip" => Ok(Self::Skip),
other => Err(Error::ConfigError(format!(
"Unknown lto_mode {other:?}; expected one of: marker, save-temps, skip"
))),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LtoFlavour {
Full,
Thin,
}
fn escape_for_assembler(path: &str) -> String {
path.replace('\\', r"\\\\").replace('"', r#"\\\""#)
}
pub fn marker_source(recorded_path: &str) -> String {
let path = escape_for_assembler(recorded_path);
format!(
r#"/* Generated by rllvm. Records the bitcode path for an LTO object. */
#if defined(__MACH__)
__asm__(".section {DARWIN_SEGMENT_NAME},{DARWIN_SECTION_NAME},regular,no_dead_strip\n.ascii \"{path}\\n\"\n.previous");
#elif defined(__ELF__)
__asm__(".section {ELF_SECTION_NAME},\"\",@progbits\n.ascii \"{path}\\n\"\n.previous");
#else
#error "rllvm: -flto bitcode extraction supports ELF and Mach-O only; set lto_mode = \"skip\""
#endif
"#
)
}
pub fn save_temps_flag(target_is_darwin: bool) -> &'static str {
if target_is_darwin {
"-Wl,-save-temps"
} else {
"-Wl,-plugin-opt=save-temps"
}
}
pub fn is_saved_module(output_name: &str, filename: &str, target_is_darwin: bool) -> bool {
if target_is_darwin {
filename == format!("{output_name}.lto.opt.bc")
} else {
let Some(rest) = filename.strip_prefix(&format!("{output_name}.")) else {
return false;
};
let Some(partition) = rest.strip_suffix(".5.precodegen.bc") else {
return false;
};
!partition.is_empty() && partition.chars().all(|c| c.is_ascii_digit())
}
}
pub fn is_save_temps_artifact(output_name: &str, filename: &str) -> bool {
let Some(rest) = filename.strip_prefix(&format!("{output_name}.")) else {
return false;
};
if rest == "rllvm.bc" {
return false;
}
if matches!(
rest,
"lto.bc" | "lto.opt.bc" | "lto.o" | "index.bc" | "index.dot" | "resolution.txt"
) {
return true;
}
if rest
.strip_prefix("lto.o")
.is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()))
{
return true;
}
let segments: Vec<&str> = rest.split('.').collect();
match segments.as_slice() {
[task, stage, name, "bc"] => {
is_digits(task)
&& is_digits(stage)
&& matches!(
*name,
"preopt" | "promote" | "internalize" | "import" | "opt" | "precodegen"
)
}
[n, "thinlto", "o"] => is_digits(n),
_ => false,
}
}
fn is_digits(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
}
pub fn user_requested_save_temps(args: &[String]) -> bool {
let mut tokens: Vec<&str> = vec![];
let mut args = args.iter();
while let Some(arg) = args.next() {
if let Some(rest) = arg.strip_prefix("-Wl,") {
tokens.extend(rest.split(','));
} else if let Some(option) = arg.strip_prefix("-Xlinker=") {
tokens.push(option);
} else if arg == "-Xlinker"
&& let Some(option) = args.next()
{
tokens.push(option);
}
}
tokens.iter().enumerate().any(|(i, token)| {
matches!(
*token,
"-save-temps" | "--save-temps" | "-plugin-opt=save-temps" | "--plugin-opt=save-temps"
) || (matches!(*token, "-plugin-opt" | "--plugin-opt")
&& tokens.get(i + 1) == Some(&"save-temps"))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lto_mode_defaults_to_marker() {
assert_eq!(LtoMode::default(), LtoMode::Marker);
}
#[test]
fn lto_mode_round_trips_through_its_documented_spelling() {
for mode in [LtoMode::Marker, LtoMode::SaveTemps, LtoMode::Skip] {
let spelled = mode.to_string();
assert_eq!(spelled.parse::<LtoMode>().unwrap(), mode, "{spelled}");
}
}
#[test]
fn unknown_lto_mode_is_an_error_not_a_fallback() {
let err = "marker ".parse::<LtoMode>().unwrap_err();
assert!(matches!(err, Error::ConfigError(_)));
assert!(err.to_string().contains("save-temps"), "{err}");
}
fn decode_one_layer(text: &str) -> String {
let mut out = String::new();
let mut chars = text.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('n') => out.push('\n'),
Some(escaped) => out.push(escaped),
None => out.push('\\'),
}
}
out
}
#[test]
fn assembler_escaping_survives_both_decoders() {
for path in [
r#"/tmp/we"ird\path.bc"#,
r"/tmp/od\d/a.bc",
r"/tmp/new\nline.bc",
"/tmp/plain.bc",
] {
let escaped = escape_for_assembler(path);
assert_eq!(
decode_one_layer(&decode_one_layer(&escaped)),
path,
"escaped as {escaped:?}"
);
}
}
#[test]
fn marker_source_terminates_the_entry_without_a_nul() {
let source = marker_source("/tmp/a.bc");
assert!(source.contains(r"\\n"), "no newline terminator: {source}");
assert!(
!source.contains(".asciz"),
"must not NUL-terminate: {source}"
);
}
#[test]
fn marker_source_covers_both_supported_formats() {
let source = marker_source("/tmp/a.bc");
assert!(
source.contains("__RLLVM,__rllvm_bc,regular,no_dead_strip"),
"{source}"
);
assert!(source.contains(".rllvm_bc,\\\"\\\",@progbits"), "{source}");
assert!(
source.contains("#error"),
"unsupported formats must not compile"
);
}
#[test]
fn save_temps_flag_matches_the_linker_family() {
assert_eq!(save_temps_flag(true), "-Wl,-save-temps");
assert_eq!(save_temps_flag(false), "-Wl,-plugin-opt=save-temps");
}
#[test]
fn saved_module_is_recognised_per_linker() {
assert!(is_saved_module("prog", "prog.lto.opt.bc", true));
assert!(!is_saved_module("prog", "prog.lto.bc", true));
assert!(is_saved_module("prog", "prog.0.5.precodegen.bc", false));
assert!(!is_saved_module("prog", "prog.0.4.opt.bc", false));
assert!(!is_saved_module("other", "prog.0.5.precodegen.bc", false));
}
#[test]
fn saved_module_does_not_collide_with_a_sibling_output_sharing_a_prefix() {
assert!(!is_saved_module(
"prog",
"prog.debug.0.5.precodegen.bc",
false
));
assert!(is_saved_module(
"prog.debug",
"prog.debug.0.5.precodegen.bc",
false
));
}
#[test]
fn cleanup_spares_the_module_rllvm_keeps_and_the_user_s_files() {
for litter in [
"prog.lto.bc",
"prog.lto.o",
"prog.lto.o1",
"prog.0.0.preopt.bc",
"prog.0.2.internalize.bc",
"prog.index.bc",
"prog.index.dot",
"prog.resolution.txt",
"prog.0.thinlto.o",
] {
assert!(is_save_temps_artifact("prog", litter), "{litter}");
}
for keep in ["prog.rllvm.bc", "prog.bc", "prog", "prog.c", "progress.bc"] {
assert!(!is_save_temps_artifact("prog", keep), "{keep}");
}
}
#[test]
fn cleanup_does_not_collide_with_a_sibling_output_sharing_a_prefix() {
for artifact in ["prog.debug.0.0.preopt.bc", "prog.debug.0.thinlto.o"] {
assert!(!is_save_temps_artifact("prog", artifact), "{artifact}");
}
for artifact in ["prog.debug.0.0.preopt.bc", "prog.debug.0.thinlto.o"] {
assert!(is_save_temps_artifact("prog.debug", artifact), "{artifact}");
}
}
#[test]
fn user_requested_save_temps_recognises_every_measured_spelling() {
for args in [
vec!["-Wl,-save-temps".to_string()],
vec!["-Wl,-O2,-save-temps".to_string()],
vec!["-Wl,--save-temps".to_string()],
vec!["-Wl,-plugin-opt=save-temps".to_string()],
vec!["-Wl,--plugin-opt=save-temps".to_string()],
vec!["-Wl,-plugin-opt,save-temps".to_string()],
vec!["-Wl,--plugin-opt,save-temps".to_string()],
vec!["-Xlinker".to_string(), "-save-temps".to_string()],
vec!["-Xlinker=-save-temps".to_string()],
vec![
"-Xlinker".to_string(),
"--plugin-opt=save-temps".to_string(),
],
vec![
"-Xlinker".to_string(),
"-plugin-opt".to_string(),
"-Xlinker".to_string(),
"save-temps".to_string(),
],
] {
assert!(user_requested_save_temps(&args), "{args:?}");
}
}
#[test]
fn user_requested_save_temps_is_false_without_the_flag() {
let args = ["-flto", "-O2", "-Wl,-dead_strip"]
.into_iter()
.map(String::from)
.collect::<Vec<String>>();
assert!(!user_requested_save_temps(&args));
for args in [
vec!["-Xlinker".to_string(), "-O2".to_string()],
vec!["-Wl,-plugin-opt=thinlto".to_string()],
] {
assert!(!user_requested_save_temps(&args), "{args:?}");
}
}
}