#![doc(html_root_url = "https://docs.rs/rucc-driver/0.10.47")]
pub mod cache;
pub mod compile;
pub mod deps;
pub mod fetch;
mod glibc;
pub mod install;
pub mod library;
pub mod link;
mod map;
pub mod phase;
pub mod preprocess;
pub mod schedule;
use std::fmt::Write as _;
use std::io::Write as _;
use std::path::PathBuf;
use rucc_codegen::coverage::{self, Fired};
use rucc_codegen::lowering::Lowerings;
use rucc_codegen::pressure::Pressure;
use rucc_pp::Dependency;
use rucc_session::{
Compress, Control, Dumps, EmitKind, Hook, Options, Pic, PrefixMap, Preinclude, Protector,
SaveTemps, Session, Std, Wrapping, runtime,
};
use rucc_sysroot::{Manifest, Sysroot};
use rucc_target::{ObjectFormat, Triple};
use rucc_tuple::TargetTuple;
use crate::link::LinkOptions;
pub use crate::compile::{Artifact, Compiled, Temps, compile, compile_ir};
pub use crate::phase::{ArchiveJob, Input, InputKind, Job, LinkJob, Output, Phase, Plan};
pub use crate::preprocess::{OsFileSystem, Preprocessed, preprocess};
pub use crate::schedule::Jobs;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Action {
Help,
Version,
Print(String),
PrintConfig(Box<Options>),
PrintPipeline(Box<Options>),
PrintPlan {
opts: Box<Options>,
plan: Box<Plan>,
link: Box<LinkOptions>,
},
Fetch {
what: &'static rucc_sysroot::Pinned,
target: TargetTuple,
cache: PathBuf,
},
Compile {
opts: Box<Options>,
plan: Box<Plan>,
link: Box<LinkOptions>,
jobs: Jobs,
verbose: bool,
notes: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CliError {
pub message: String,
}
impl std::fmt::Display for CliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for CliError {}
fn err(message: impl Into<String>) -> CliError {
CliError { message: message.into() }
}
fn rewrite<'a>(arg: &'a str, flag: &str) -> Result<(&'a str, &'a str), CliError> {
let rest = &arg[flag.len()..];
PrefixMap::split(rest).ok_or_else(|| {
let flag = flag.trim_end_matches('=');
err(format!(
"`{rest}` is not a rewrite for `{flag}`, which is an old prefix, an `=` and a new one"
))
})
}
enum Query {
Machine,
Version,
Multiarch,
SearchDirs,
Sysroot,
SysrootProvenance,
SysrootDigest,
FileName(String),
ProgName(String),
Libgcc,
}
pub const USAGE: &str = "\
rucc, an optimizing C compiler
usage: rucc [options] file...
options:
-c compile and assemble, do not link
-S compile only, emit assembly
-E preprocess only
-o <file> write output to <file>, or to standard output for -
-D <name>[=<value>], -U <name> define a macro, or undefine one after every -D
-I <dir> add <dir> to the include search path
-iquote -isystem -idirafter <dir> the other chains, -nostdinc drops ours
-I-, -iprefix <p>, -iwithprefix[before] <dir> the older spellings of those
-include <file>, -imacros <file> read <file> first, the second for its macros only
--sysroot=<dir> look for the library's headers under <dir>, -isysroot too
-P, -dM with -E: leave out the markers, or dump the macros
-M -MM -MD -MMD write a make rule for the source, the last two compile as well
-MF <file> -MT <t> -MQ <t> -MP where the rule goes, what it builds, targets with no recipe
-std=<dialect> c89 through c23, and the gnu spellings
-fgnuc-version=<v> the GCC release to claim, default 7.0.0
-x <lang> treat later inputs as <lang>, or none to stop
-O<level> optimize: 0, 1, 2, 3, s, z
-fsafety=<tier> check memory safety: off, detect, enforce, kernel
-f[no-]sanitize=<what> the negative is taken, the positive is refused by name
-f[no-]safety-subobject a write has to stay inside the member it names
-f[no-]safety-restrict two restrict pointers of one block may not meet
-f<pass> -fno-<pass> -fdump-ir=<what> -fopt-info[-<kind>][=FILE]
-fpass-fuel=<pass>=<n>, -fpass-fuel-global=<n> stop a pass, or all of them, after n
-fdisable-<pass>[=<funcs>], -fenable-<pass>[=<funcs>] run a pass on some functions only
-g -g0 -gdwarf-5, -fno-omit-frame-pointer, -mno-red-zone debug info, frame pointer, red zone
-gz[=none|zlib|zlib-gnu|zstd] -gno-split-dwarf compress debug sections, one file not two
-flto[=auto|jobserver|<n>] -fno-lto -ffat-lto-objects read, and not done yet
-fprofile-use[=<path>] -fprofile-dir=<dir> read too, where -fprofile-generate is refused
-f[no-]stack-protector[-strong|-all], -f[no-]stack-clash-protection, -fcf-protection=<edges>
-ffunction-sections -fdata-sections a section per function or variable, for --gc-sections
-fvisibility=<what> default, hidden, internal or protected, when nothing in the source said
-l<name>, -L <dir>, -B <dir> link a library, where to look for one, where our own tools are
-fPIC -fpic -fPIE -fpie, -fno-common, -pipe what it does anyway
-f[no-]strict-aliasing, -f[no-]delete-null-pointer-checks what it assumes anyway
-static -shared -pie -no-pie -nostdlib -nostartfiles -nodefaultlibs -rdynamic -s how to link
-Wl,<arg>, -Xlinker <arg>, -fuse-ld=<name> hand an argument to the linker, or pick one
-Werror -pedantic -pedantic-errors -w how much to say, and whether it is fatal
-m64 -march= -mtune= -mcpu= -mabi= -mcmodel= what machine to generate for
-pg -p, -mfentry -mno-fentry call a profiler on the way in, and where that call goes
-fpatchable-function-entry=<n>[,<m>] room at the top of every function to patch later
-fwrapv, -fwrapv-pointer, -fno-strict-overflow signed or pointer overflow wraps
-ftrapv signed overflow stops the program instead
-f[no-]signed-char, -f[no-]unsigned-char, -f[no-]short-enums change the ABI
-ffp-contract=<how> fuse a multiply and an addition: fast, on or off
-fexcess-precision=<how>, -f[no-]rounding-math, -f[no-]trapping-math what may be folded
-ffile-prefix-map=<old>=<new> rewrite that front of every path we put in the output
-fmacro-prefix-map= -fdebug-prefix-map= -fprofile-prefix-map= the same, one output each
-pthread build for more than one thread, and link the library for it
-dumpmachine -dumpversion -print-multiarch -print-search-dirs what this compiler is
-print-file-name=<name> -print-prog-name=<name> where a file or a program is
-print-sysroot the root the headers and the libraries are read under
-print-sysroot-provenance every input under it, where it came from and its licence
-print-sysroot-digest the sha256 of that record, which names the whole sysroot in one line
--fetch <tuple> get the sysroot this release pins for <tuple> and install it in the cache
--offline never download anything, which a compilation never does anyway
-j[n] compile n translation units at once, default all
-v, -### print each phase as it runs, or without running any
-save-temps[=cwd|obj], -time keep the .i and the .s, say how long each step took
--target=<triple> generate code for <triple>
--emit=<kind> exe, obj, archive, asm, preprocessed, tast, ir, mir-final,
safety-summary, type-granules
--print-config, --print-pipeline print the configuration or the pipeline, and exit
--version print the version and exit
-h, --help print this message and exit
See spec/04-driver-and-cli.md for the full flag reference.
";
fn joined_or_next(
arg: &str,
at: usize,
args: &[String],
i: &mut usize,
) -> Result<String, CliError> {
if arg.len() > at {
return Ok(arg[at..].to_owned());
}
let next = args.get(*i).ok_or_else(|| err(format!("{arg} requires an argument")))?;
*i += 1;
Ok(next.clone())
}
const SANITIZERS: [&str; 34] = [
"address",
"kernel-address",
"hwaddress",
"kernel-hwaddress",
"pointer-compare",
"pointer-subtract",
"thread",
"leak",
"undefined",
"shift",
"shift-base",
"shift-exponent",
"integer-divide-by-zero",
"unreachable",
"vla-bound",
"null",
"return",
"signed-integer-overflow",
"bounds",
"bounds-strict",
"alignment",
"object-size",
"float-divide-by-zero",
"float-cast-overflow",
"nonnull-attribute",
"returns-nonnull-attribute",
"bool",
"enum",
"vptr",
"pointer-overflow",
"builtin",
"alias",
"restrict",
"memory",
];
pub fn parse_args(args: &[String]) -> Result<Action, CliError> {
let host = Triple::host()
.ok_or_else(|| err("this host is not a supported target and no --target was given"))?;
let mut opts = Options::new(host);
let mut inputs: Vec<Input> = Vec::new();
let mut print_config = false;
let mut print_pipeline = false;
let mut print_plan = false;
let mut verbose = false;
let mut jobs = Jobs::default();
let mut nostdinc = false;
let mut sysroot: Option<PathBuf> = None;
let mut notes: Vec<String> = Vec::new();
let mut pinned: Option<TargetTuple> = None;
let mut output = None;
let mut link = LinkOptions::default();
let mut query: Option<Query> = None;
let mut fetch: Option<String> = None;
let mut offline = false;
let mut threads = false;
let mut sanitizers: Vec<&str> = Vec::new();
let mut forced: Option<InputKind> = None;
let mut iprefix = String::new();
let mut i = 0;
while i < args.len() {
let arg = args[i].as_str();
i += 1;
match arg {
"-h" | "--help" => return Ok(Action::Help),
"--version" => return Ok(Action::Version),
"--fetch" => {
let value = args
.get(i)
.ok_or_else(|| err("--fetch requires the target to get a sysroot for"))?;
i += 1;
fetch = Some(value.clone());
}
_ if arg.starts_with("--fetch=") => {
fetch = Some(arg["--fetch=".len()..].to_owned());
}
"--offline" => offline = true,
"--print-config" => print_config = true,
"--print-pipeline" => print_pipeline = true,
"-###" => print_plan = true,
"-v" => verbose = true,
"-save-temps" => opts.save_temps = SaveTemps::Object,
_ if arg.starts_with("-save-temps=") => {
opts.save_temps = arg["-save-temps=".len()..].parse().map_err(err)?;
}
"-time" => opts.time = true,
"-c" => opts.emit = EmitKind::Object,
"-S" => opts.emit = EmitKind::Asm,
"-E" => opts.emit = EmitKind::Preprocessed,
"-g" => opts.debug_info = true,
"-g0" => opts.debug_info = false,
"-g1" | "-g2" | "-g3" | "-ggdb" | "-ggdb1" | "-ggdb2" | "-ggdb3" => {
opts.debug_info = true;
}
"-gdwarf" | "-gdwarf-5" => opts.debug_info = true,
_ if arg.starts_with("-gdwarf-") => {
return Err(err(format!(
"{arg}: this compiler writes DWARF 5 and no other version, see \
spec/11-debug-info.md"
)));
}
"-gno-split-dwarf" => {}
"-gsplit-dwarf" => {
return Err(err(format!(
"{arg}: this compiler writes no separate `.dwo` file, and a build that \
expects one beside each object would wait for a file that never arrives, \
see spec/11-debug-info.md"
)));
}
"-gz" => opts.compress = Compress::Zlib,
_ if arg.starts_with("-gz=") => {
let how = &arg["-gz=".len()..];
opts.compress = how.parse().map_err(|()| {
err(format!(
"`{how}` is not a way to compress debug sections, which is none, zlib, \
zlib-gnu or zstd"
))
})?;
}
"-Werror" => opts.warnings_are_errors = true,
"-w" => opts.warnings = false,
"-pedantic-errors" => {
opts.pedantic = true;
opts.warnings_are_errors = true;
}
"-P" => opts.line_markers = false,
"-M" => {
opts.deps.emit = true;
opts.deps.instead_of_compiling = true;
}
"-MM" => {
opts.deps.emit = true;
opts.deps.instead_of_compiling = true;
opts.deps.system_headers = false;
}
"-MD" => opts.deps.emit = true,
"-MMD" => {
opts.deps.emit = true;
opts.deps.system_headers = false;
}
"-MP" => opts.deps.phony = true,
"-MF" | "-MT" | "-MQ" => {
let value =
args.get(i).ok_or_else(|| err(format!("{arg} requires an argument")))?;
i += 1;
match arg {
"-MF" => opts.deps.file = Some(value.clone()),
"-MT" => opts.deps.targets.push(value.clone()),
_ => opts.deps.targets.push(deps::escaped(value)),
}
}
"-dumpmachine" => query = Some(Query::Machine),
"-dumpversion" | "-dumpfullversion" => query = Some(Query::Version),
"-print-multiarch" => query = Some(Query::Multiarch),
"-print-search-dirs" => query = Some(Query::SearchDirs),
"-print-sysroot" => query = Some(Query::Sysroot),
"-print-sysroot-provenance" | "--print-sysroot-provenance" => {
query = Some(Query::SysrootProvenance);
}
"-print-sysroot-digest" | "--print-sysroot-digest" => {
query = Some(Query::SysrootDigest);
}
"-print-libgcc-file-name" => query = Some(Query::Libgcc),
_ if arg.starts_with("-print-file-name=") => {
query = Some(Query::FileName(arg["-print-file-name=".len()..].to_owned()));
}
_ if arg.starts_with("-print-prog-name=") => {
query = Some(Query::ProgName(arg["-print-prog-name=".len()..].to_owned()));
}
"-pthread" | "-pthreads" => {
opts.defines.push("_REENTRANT".to_owned());
threads = true;
}
"-ansi" => {
opts.std = Std::C89;
opts.gnu_extensions = false;
}
"-pedantic" | "-Wpedantic" => opts.pedantic = true,
"-fpermissive" => opts.permissive = true,
"-fno-permissive" => opts.permissive = false,
"-ffreestanding" => opts.hosted = false,
"-fhosted" => opts.hosted = true,
"-fno-builtin" => opts.builtins = false,
"-fbuiltin" => opts.builtins = true,
"-fgnu89-inline" => opts.gnu89_inline = true,
"-fno-gnu89-inline" => opts.gnu89_inline = false,
"-fno-omit-frame-pointer" => opts.frame_pointer = true,
"-fomit-frame-pointer" => opts.frame_pointer = false,
"-freorder-blocks" => opts.reorder_blocks = Some(true),
"-fno-reorder-blocks" => opts.reorder_blocks = Some(false),
"-fschedule-insns2" => opts.schedule_insns = Some(true),
"-fno-schedule-insns2" => opts.schedule_insns = Some(false),
"-mno-red-zone" => opts.red_zone = false,
"-mred-zone" => opts.red_zone = true,
"-fno-stack-protector" | "-fno-stack-protector-all" | "-fno-stack-protector-strong" => {
opts.protector = Protector::None;
}
"-fstack-protector" => opts.protector = Protector::Buffers,
"-fstack-protector-strong" => opts.protector = Protector::Strong,
"-fstack-protector-all" => opts.protector = Protector::All,
"-fstack-clash-protection" => opts.stack_clash = true,
"-fno-stack-clash-protection" => opts.stack_clash = false,
"-fcf-protection" => opts.control = Control::Full,
"-fno-cf-protection" => opts.control = Control::None,
"-pg" | "-p" => {
opts.profile = true;
link.profile = true;
}
"-mfentry" => opts.hook = Hook::Early,
"-mno-fentry" => opts.hook = Hook::Late,
"-nostdinc" => nostdinc = true,
"-o" => {
output = Some(args.get(i).ok_or_else(|| err("-o requires an argument"))?.clone());
i += 1;
}
"-isysroot" => {
let dir = args.get(i).ok_or_else(|| err("-isysroot requires an argument"))?;
i += 1;
sysroot = Some(PathBuf::from(dir));
}
"-iquote" | "-isystem" | "-idirafter" => {
let dir = args.get(i).ok_or_else(|| err(format!("{arg} requires an argument")))?;
i += 1;
match arg {
"-iquote" => opts.search.push_quote(dir.clone()),
"-isystem" => opts.search.push_system(dir.clone()),
_ => opts.search.push_after(dir.clone()),
}
}
"-iprefix" => {
iprefix = args.get(i).ok_or_else(|| err("-iprefix requires an argument"))?.clone();
i += 1;
}
"-iwithprefix" | "-iwithprefixbefore" => {
let dir = args.get(i).ok_or_else(|| err(format!("{arg} requires an argument")))?;
i += 1;
let dir = format!("{iprefix}{dir}");
if arg == "-iwithprefix" {
opts.search.push_system(dir);
} else {
opts.search.push_bracket(dir);
}
}
"-include" | "-imacros" => {
let name = args.get(i).ok_or_else(|| err(format!("{arg} requires an argument")))?;
i += 1;
opts.preincludes
.push(Preinclude { name: name.clone(), macros_only: arg == "-imacros" });
}
"-I-" => opts.search.split_quote_chain(),
"-x" => {
let lang = args.get(i).ok_or_else(|| err("-x requires an argument"))?;
i += 1;
forced = if lang == "none" {
None
} else {
Some(InputKind::from_x_arg(lang).map_err(|e| err(format!("{e}")))?)
};
}
_ if arg.starts_with("-D") => {
let value = joined_or_next(arg, 2, args, &mut i)?;
opts.defines.push(value);
}
_ if arg.starts_with("-U") => {
let value = joined_or_next(arg, 2, args, &mut i)?;
opts.undefines.push(value);
}
_ if arg.starts_with("-I") => {
let dir = joined_or_next(arg, 2, args, &mut i)?;
opts.search.push_bracket(dir);
}
_ if arg.starts_with("-std=") => {
let name = &arg["-std=".len()..];
let (std, gnu) = Std::from_flag(name)
.ok_or_else(|| err(format!("unknown dialect `{name}`, see --help")))?;
opts.std = std;
opts.gnu_extensions = gnu;
}
_ if Dumps::is_family(arg) => {
opts.dumps.add(&arg[2..]);
}
_ if arg.starts_with("-fno-builtin-") => {
opts.no_builtin.push(arg["-fno-builtin-".len()..].to_owned());
}
_ if arg.starts_with("-fgnuc-version=") => {
let v = &arg["-fgnuc-version=".len()..];
opts.gnuc = v.parse().map_err(err)?;
}
"-fnested-functions" => {
return Err(err(
"nested functions are not supported: a call to one goes through a trampoline \
written on the stack, which no target that enforces an unexecutable stack \
allows",
));
}
"-fno-nested-functions" => {}
"-fPIC" | "-fpic" => opts.pic = Pic::Library,
"-fPIE" | "-fpie" => opts.pic = Pic::Executable,
"-fsemantic-interposition" => opts.interposition = true,
"-fno-semantic-interposition" => opts.interposition = false,
"-fasynchronous-unwind-tables" => opts.async_unwind_tables = true,
"-fno-asynchronous-unwind-tables" => opts.async_unwind_tables = false,
"-funwind-tables" => opts.unwind_tables = true,
"-fno-unwind-tables" => opts.unwind_tables = false,
"-fno-pic" | "-fno-pie" => {
return Err(err(
"position dependent code is not supported: an address that may be in another \
object is loaded out of the global offset table, and nothing here emits the \
absolute form this asks for. Use -no-pie if what you meant was how to link",
));
}
"-ffunction-sections" => opts.function_sections = true,
"-fno-function-sections" => opts.function_sections = false,
"-fdata-sections" => opts.data_sections = true,
"-fno-data-sections" => opts.data_sections = false,
"-fno-common" => {}
"-fwrapv" => {
opts.wrapping.signed = true;
opts.wrapping.trap = false;
}
"-fno-wrapv" => opts.wrapping.signed = false,
"-fwrapv-pointer" => opts.wrapping.pointer = true,
"-fno-wrapv-pointer" => opts.wrapping.pointer = false,
"-fno-strict-overflow" => opts.wrapping = Wrapping::ALL,
"-fstrict-overflow" => {
opts.wrapping.signed = false;
opts.wrapping.pointer = false;
}
"-ftrapv" => {
opts.wrapping.trap = true;
opts.wrapping.signed = false;
}
"-fno-trapv" => opts.wrapping.trap = false,
"-fsigned-char" | "-fno-unsigned-char" => opts.char_signed = Some(true),
"-funsigned-char" | "-fno-signed-char" => opts.char_signed = Some(false),
"-fshort-enums" => opts.short_enums = true,
"-fno-short-enums" => opts.short_enums = false,
"-fcommon" => {
return Err(err(
"a tentative definition is written into .bss as its own symbol here, and \
nothing emits the common symbol this asks the linker to merge. Give the \
variable a definition in one file and declare it extern in the others",
));
}
"-fstrict-aliasing" => opts.strict_aliasing = true,
"-fno-strict-aliasing" => opts.strict_aliasing = false,
"-fdelete-null-pointer-checks" | "-fno-delete-null-pointer-checks" => {}
"-frounding-math" | "-fno-rounding-math" => {}
"-ftrapping-math" => opts.trapping_math = true,
"-fno-trapping-math" => opts.trapping_math = false,
"-pipe" => {}
"-fdiagnostics-color" | "-fno-diagnostics-color" => {}
_ if arg.starts_with("-fdiagnostics-color=") => {}
"-static" => link.is_static = true,
"-shared" => link.shared = true,
"-pie" => link.pie = Some(true),
"-no-pie" | "-nopie" => link.pie = Some(false),
"-nostdlib" => link.no_stdlib = true,
"-nostartfiles" => link.no_startfiles = true,
"-nodefaultlibs" => link.no_defaultlibs = true,
"-fno-builtins-lib" => link.no_builtins_lib = true,
"-fbuiltins-lib" => link.no_builtins_lib = false,
"-rdynamic" | "-export-dynamic" => link.export_dynamic = true,
"-s" => link.strip = true,
"-Xlinker" => {
let next = args.get(i).ok_or_else(|| err("-Xlinker requires an argument"))?;
i += 1;
link.passthrough.push(next.clone());
}
_ if arg.starts_with("-Wl,") => {
link.passthrough.extend(arg["-Wl,".len()..].split(',').map(str::to_owned));
}
_ if arg.starts_with("-fuse-ld=") => {
link.use_ld = Some(arg["-fuse-ld=".len()..].to_owned());
}
_ if arg.starts_with("-l") && arg.len() > 2 => {
inputs.push(Input::library(&arg[2..]));
}
"-l" => {
let next = args.get(i).ok_or_else(|| err("-l requires an argument"))?;
i += 1;
inputs.push(Input::library(next));
}
_ if arg.starts_with("-L") => {
link.search.push(PathBuf::from(joined_or_next(arg, 2, args, &mut i)?));
}
_ if arg.starts_with("-B") => {
link.prefixes.push(PathBuf::from(joined_or_next(arg, 2, args, &mut i)?));
}
_ if arg.starts_with("-j") => {
jobs = Jobs::parse(&arg[2..]).map_err(err)?;
}
_ if arg.starts_with("--sysroot=") => {
sysroot = Some(PathBuf::from(&arg["--sysroot=".len()..]));
}
_ if arg.starts_with("--target=") => {
let t = &arg["--target=".len()..];
opts.target = t.parse().map_err(|e| err(format!("{e}")))?;
pinned = t.parse().ok();
}
_ if arg.starts_with("--emit=") => {
let k = &arg["--emit=".len()..];
opts.emit = k
.parse()
.map_err(|()| err(format!("unknown --emit kind `{k}`, see --help")))?;
}
"-O" | "-Og" => opts.opt_level = rucc_session::OptLevel::O1,
"-Ofast" => {
return Err(err(
"-Ofast is -O3 with fast math, and fast math is not implemented, see \
spec/04-driver-and-cli.md section 4.6",
));
}
_ if arg.starts_with("-O") => {
opts.opt_level = arg[2..]
.parse()
.map_err(|()| err(format!("unknown optimization level `{arg}`")))?;
}
_ if arg.starts_with("-ffp-contract=") => {
let how = &arg["-ffp-contract=".len()..];
opts.fp_contract = how.parse().map_err(|()| {
err(format!("`{how}` is not a contraction, which is fast, on or off"))
})?;
}
_ if arg.starts_with("-fstack-reuse=") => {
let how = &arg["-fstack-reuse=".len()..];
opts.stack_reuse = match how {
"all" | "named_vars" => Some(true),
"none" => Some(false),
_ => {
return Err(err(format!(
"`{how}` is not a stack reuse, which is all, named_vars or none"
)));
}
};
}
_ if arg.starts_with("-fexcess-precision=") => {
let how = &arg["-fexcess-precision=".len()..];
if !matches!(how, "16" | "fast" | "standard") {
return Err(err(format!(
"`{how}` is not an excess precision, which is 16, fast or standard"
)));
}
}
_ if arg.starts_with("-fmacro-prefix-map=") => {
let (old, new) = rewrite(arg, "-fmacro-prefix-map=")?;
opts.prefix_map.macros.push(old, new);
}
_ if arg.starts_with("-fdebug-prefix-map=") => {
let (old, new) = rewrite(arg, "-fdebug-prefix-map=")?;
opts.prefix_map.debug.push(old, new);
}
_ if arg.starts_with("-fprofile-prefix-map=") => {
let (old, new) = rewrite(arg, "-fprofile-prefix-map=")?;
opts.prefix_map.profile.push(old, new);
}
_ if arg.starts_with("-ffile-prefix-map=") => {
let (old, new) = rewrite(arg, "-ffile-prefix-map=")?;
opts.prefix_map.macros.push(old, new);
opts.prefix_map.debug.push(old, new);
opts.prefix_map.profile.push(old, new);
}
"-flto" => opts.lto.requested = true,
"-fno-lto" => opts.lto.requested = false,
_ if arg.starts_with("-flto=") => {
let how = &arg["-flto=".len()..];
opts.lto.jobs = how.parse().map_err(|()| {
err(format!(
"`{how}` is not a number of link time jobs, which is auto, jobserver or a \
count above zero"
))
})?;
opts.lto.requested = true;
}
_ if arg.starts_with("-flto-partition=") => {
let how = &arg["-flto-partition=".len()..];
opts.lto.partition = how.parse().map_err(|()| {
err(format!(
"`{how}` is not a partitioning model, which is balanced, 1to1, one, max \
or none"
))
})?;
}
_ if arg.starts_with("-flto-compression-level=") => {
let how = &arg["-flto-compression-level=".len()..];
let level =
how.parse::<u8>().ok().filter(|level| *level <= 19).ok_or_else(|| {
err(format!("`{how}` is not a compression level, 0 to 19"))
})?;
opts.lto.compression = Some(level);
}
"-ffat-lto-objects" | "-fno-fat-lto-objects" => {}
"-fuse-linker-plugin" | "-fno-use-linker-plugin" => {}
"-fprofile-use" => opts.profile_data.requested = true,
"-fno-profile-use" => opts.profile_data.requested = false,
_ if arg.starts_with("-fprofile-use=") => {
opts.profile_data.path = Some(arg["-fprofile-use=".len()..].to_string());
opts.profile_data.requested = true;
}
_ if arg.starts_with("-fprofile-dir=") => {
opts.profile_data.dir = Some(arg["-fprofile-dir=".len()..].to_string());
}
"-fprofile-abs-path" => opts.profile_data.absolute = true,
"-fno-profile-abs-path" => opts.profile_data.absolute = false,
"-fprofile-correction" => opts.profile_data.correction = true,
"-fno-profile-correction" => opts.profile_data.correction = false,
"-fprofile-partial-training" => opts.profile_data.partial_training = true,
"-fno-profile-partial-training" => opts.profile_data.partial_training = false,
"-fprofile-arcs"
| "--coverage"
| "-fcondition-coverage"
| "-fpath-coverage"
| "-fprofile-generate" => {
return Err(err(format!(
"{arg}: this compiler does not instrument for profiling, and a build that \
expects the counts a run of the instrumented program writes would optimize \
against nothing on its second pass, see spec/04-driver-and-cli.md"
)));
}
_ if arg.starts_with("-fprofile-generate=") => {
return Err(err(format!(
"{arg}: this compiler does not instrument for profiling, and a build that \
expects the counts a run of the instrumented program writes would optimize \
against nothing on its second pass, see spec/04-driver-and-cli.md"
)));
}
"-ftest-coverage" => {
return Err(err(format!(
"{arg}: this compiler writes no `.gcno` file beside the object, and a build \
that expects one would wait for a file that never arrives, see \
spec/04-driver-and-cli.md"
)));
}
_ if arg.starts_with("-fprofile-update=") => {
let how = &arg["-fprofile-update=".len()..];
if !matches!(how, "single" | "atomic" | "prefer-atomic") {
return Err(err(format!(
"`{how}` is not a profile update method, which is single, atomic or \
prefer-atomic"
)));
}
}
_ if arg.starts_with("-fprofile-reproducible=") => {
let how = &arg["-fprofile-reproducible=".len()..];
if !matches!(how, "serial" | "parallel-runs" | "multithreaded") {
return Err(err(format!(
"`{how}` is not a profile reproducibility method, which is serial, \
parallel-runs or multithreaded"
)));
}
}
"-fprofile-values" | "-fno-profile-values" | "-fprofile-info-section" => {}
"-fno-test-coverage" | "-fno-profile-arcs" | "-fno-profile-generate" => {}
_ if arg.starts_with("-fprofile-filter-files=")
|| arg.starts_with("-fprofile-exclude-files=")
|| arg.starts_with("-fprofile-note=") => {}
_ if arg.starts_with("-fvisibility=") => {
let seen = &arg["-fvisibility=".len()..];
opts.visibility = seen.parse().map_err(|()| {
err(format!(
"`{seen}` is not a visibility, which is default, hidden, internal or \
protected"
))
})?;
}
_ if arg.starts_with("-fcf-protection=") => {
let edges = &arg["-fcf-protection=".len()..];
opts.control = edges.parse().map_err(|()| {
err(format!(
"`{edges}` is not a control flow protection, which is full, branch, \
return, none or check"
))
})?;
}
_ if arg.starts_with("-fpatchable-function-entry=") => {
let room = &arg["-fpatchable-function-entry=".len()..];
opts.patchable = room.parse().map_err(|()| {
err(format!(
"`{room}` is not an amount of room to reserve, which is a number of bytes and then, after a comma, how many of them go in front of the function's own label"
))
})?;
}
_ if arg.starts_with("-fsafety=") => {
let tier = &arg["-fsafety=".len()..];
opts.safety = tier.parse().map_err(|()| {
err(format!(
"`{tier}` is not a safety tier, which is off, detect, enforce or kernel"
))
})?;
}
_ if arg.starts_with("-fsafety-init=") => {
let mode = &arg["-fsafety-init=".len()..];
opts.padding = mode.parse().map_err(|()| {
err(format!("`{mode}` is not a padding mode, which is padding or nopadding"))
})?;
}
"-fsafety-subobject" => opts.subobject = rucc_session::Subobject::Members,
"-fno-safety-subobject" => opts.subobject = rucc_session::Subobject::Off,
_ if arg.starts_with("-fsafety-subobject=") => {
let form = &arg["-fsafety-subobject=".len()..];
return Err(err(format!(
"`{form}` is not a form of -fsafety-subobject. The flag takes no value, and \
the strict form of section 9.4 is tamnd/rucc#967"
)));
}
"-fsafety-restrict" => opts.promise = rucc_session::Promise::Blocks,
"-fno-safety-restrict" => opts.promise = rucc_session::Promise::Off,
_ if arg.starts_with("-fsafety-restrict=") => {
let form = &arg["-fsafety-restrict=".len()..];
return Err(err(format!(
"`{form}` is not a form of -fsafety-restrict. The flag takes no value."
)));
}
_ if arg.starts_with("-fsafety-races=") => {
let mode = &arg["-fsafety-races=".len()..];
opts.races = mode.parse().map_err(|()| {
err(format!("`{mode}` is not a race mode, which is off, metadata or pointer"))
})?;
}
"-fno-safety-races" => opts.races = rucc_session::Races::Off,
_ if arg.starts_with("-fsanitize=") => {
for one in arg["-fsanitize=".len()..].split(',') {
if one == "all" {
return Err(err(
"`-fsanitize=all` is not a gcc option, only `-fno-sanitize=all` is",
));
}
if !SANITIZERS.contains(&one) {
return Err(err(format!(
"`{one}` is not a sanitizer, see spec/04-driver-and-cli.md section 4.7"
)));
}
if !sanitizers.contains(&one) {
sanitizers.push(one);
}
}
}
_ if arg.starts_with("-fno-sanitize=") => {
for one in arg["-fno-sanitize=".len()..].split(',') {
if one == "all" {
sanitizers.clear();
continue;
}
if !SANITIZERS.contains(&one) {
return Err(err(format!(
"`{one}` is not a sanitizer, see spec/04-driver-and-cli.md section 4.7"
)));
}
sanitizers.retain(|asked| *asked != one);
}
}
_ if arg.starts_with("-fsanitize-recover=")
|| arg.starts_with("-fno-sanitize-recover=")
|| arg.starts_with("-fsanitize-trap=")
|| arg.starts_with("-fno-sanitize-trap=") =>
{
let how = arg.split_once('=').map_or("", |(_, rest)| rest);
for one in how.split(',') {
if one != "all" && !SANITIZERS.contains(&one) {
return Err(err(format!(
"`{one}` is not a sanitizer, see spec/04-driver-and-cli.md section 4.7"
)));
}
}
}
"-fsanitize-undefined-trap-on-error"
| "-fsanitize-address-use-after-scope"
| "-fno-sanitize-address-use-after-scope" => {}
_ if arg.starts_with("-fsanitize-sections=") => {}
_ if arg.starts_with("-fsanitize-coverage=") => {
let how = &arg["-fsanitize-coverage=".len()..];
for one in how.split(',') {
if !matches!(one, "trace-pc" | "trace-cmp") {
return Err(err(format!(
"`{one}` is not a coverage instrumentation, which is trace-pc or \
trace-cmp"
)));
}
}
return Err(err(format!(
"{arg}: this compiler generates no coverage callbacks, and a fuzzer built \
with it would run without any feedback at all, see \
spec/04-driver-and-cli.md section 4.7"
)));
}
_ if arg.starts_with("-fpass-fuel=") => {
let (name, count) = arg["-fpass-fuel=".len()..]
.split_once('=')
.ok_or_else(|| err("-fpass-fuel= is spelled <pass>=<count>"))?;
if rucc_opt::pass::find(name).is_none() {
return Err(err(format!(
"`{name}` is not a pass this compiler has, see --print-pipeline"
)));
}
let count: u32 = count
.parse()
.map_err(|_| err(format!("`{count}` is not a number of transformations")))?;
opts.pass_fuel.push((name.to_owned(), count));
}
_ if arg.starts_with("-fpass-fuel-global=") => {
let count = &arg["-fpass-fuel-global=".len()..];
let count: u32 = count
.parse()
.map_err(|_| err(format!("`{count}` is not a number of transformations")))?;
opts.pass_fuel_global = Some(count);
}
_ if arg == "-fopt-info"
|| arg.starts_with("-fopt-info=")
|| arg.starts_with("-fopt-info-") =>
{
let rest = &arg["-fopt-info".len()..];
let (kinds, file) = match rest.split_once('=') {
Some((kinds, file)) => (kinds, Some(file)),
None => (rest, None),
};
let kinds = kinds.strip_prefix('-').unwrap_or(kinds);
rucc_opt::Wants::none().add(kinds).map_err(err)?;
opts.opt_info.push(kinds.to_owned());
if let Some(file) = file {
if file.is_empty() {
return Err(err("-fopt-info= was given no file to write to"));
}
opts.opt_info_file = Some(file.to_owned());
}
}
_ if arg.starts_with("-fdump-ir=") => {
let spec = &arg["-fdump-ir=".len()..];
rucc_opt::Dumps::default().add(spec).map_err(err)?;
opts.dump_ir.push(spec.to_owned());
}
_ if arg.starts_with("-fdisable-") || arg.starts_with("-fenable-") => {
let on = arg.starts_with("-fenable-");
let spec = &arg[if on { "-fenable-".len() } else { "-fdisable-".len() }..];
rucc_opt::Gates::default().add(on, spec).map_err(err)?;
opts.pass_gates.push((on, spec.to_owned()));
}
"-funroll-loops" => opts.passes.push(("unroll".to_owned(), true)),
"-fno-unroll-loops" => opts.passes.push(("unroll".to_owned(), false)),
_ if arg.strip_prefix("-fno-").is_some_and(|n| rucc_opt::pass::find(n).is_some()) => {
opts.passes.push((arg["-fno-".len()..].to_owned(), false));
}
_ if arg.strip_prefix("-f").is_some_and(|n| rucc_opt::pass::find(n).is_some()) => {
opts.passes.push((arg["-f".len()..].to_owned(), true));
}
_ if arg.starts_with("-ftree-") || arg.starts_with("-fno-tree-") => {}
_ if arg.starts_with("-fipa-") || arg.starts_with("-fno-ipa-") => {}
"-fexpensive-optimizations" | "-fno-expensive-optimizations" => {}
"-fmodulo-sched" | "-fno-modulo-sched" => {}
"-fvect-cost-model" | "-fno-vect-cost-model" => {}
_ if arg.starts_with("-fvect-cost-model=") || arg.starts_with("-fsimd-cost-model=") => {
}
"-fearly-inlining" | "-fno-early-inlining" => {}
"-finline"
| "-fno-inline"
| "-finline-functions"
| "-fno-inline-functions"
| "-finline-small-functions"
| "-fno-inline-small-functions"
| "-finline-functions-called-once"
| "-fno-inline-functions-called-once" => {}
"-foptimize-strlen" | "-fno-optimize-strlen" => {}
"-fira-share-spill-slots" | "-fno-ira-share-spill-slots" => {}
_ if arg.starts_with("-finput-charset=") => {
let name = &arg["-finput-charset=".len()..];
if !name.eq_ignore_ascii_case("utf-8") && !name.eq_ignore_ascii_case("utf8") {
return Err(err(format!(
"-finput-charset={name}: the preprocessor reads UTF-8 and has no \
converter, so a file in another encoding would be read as though it were \
UTF-8 rather than converted",
)));
}
}
"-ffast-math" => {
return Err(err(
"-ffast-math is a licence to answer a floating point arithmetic differently \
from the way the source wrote it, and it is not one flag: it defines \
__FAST_MATH__, which a library header reads, and gcc links a startup file \
that puts the hardware in flush to zero mode for the whole process. Taking it \
and dropping it would change what other objects in the same program answer. \
-ffp-contract= and -fexcess-precision= are the parts of it this compiler has",
));
}
"-fno-fast-math" => {}
"-fnon-call-exceptions" => {
return Err(err(
"-fnon-call-exceptions is a promise that an instruction which is not a call \
can raise an exception the unwinder finds a handler for, and nothing here \
produces a landing pad for a trapping instruction. A program built without it \
would unwind past the handler it wrote",
));
}
"-fno-non-call-exceptions" => {}
"-finstrument-functions" => {
return Err(err(
"-finstrument-functions calls __cyg_profile_func_enter on entry to every \
function and __cyg_profile_func_exit on the way out, and nothing here emits \
either call. A program that asks for them usually counts them, so taking the \
flag and dropping it would turn a program that fails loudly into one that \
fails quietly",
));
}
"-fno-instrument-functions" => {}
"-Zverify-each" => opts.verify_each = true,
_ if arg.starts_with("-Zrule-coverage=") => {
let file = &arg["-Zrule-coverage=".len()..];
if file.is_empty() {
return Err(err("-Zrule-coverage= needs a file to write to"));
}
opts.rule_coverage = Some(file.to_owned());
}
_ if arg.starts_with("-Zcycle-accurate-model=") => {
let value = &arg["-Zcycle-accurate-model=".len()..];
opts.cycle_accurate_model = match value {
"yes" | "1" => Some(true),
"no" | "0" => Some(false),
_ => {
return Err(err("-Zcycle-accurate-model= takes yes or no"));
}
};
}
_ if arg.starts_with("-Zlowering=") => {
let file = &arg["-Zlowering=".len()..];
if file.is_empty() {
return Err(err("-Zlowering= needs a file to write to"));
}
opts.lowering_dump = Some(file.to_owned());
}
_ if arg.starts_with("-Zregister-pressure=") => {
let file = &arg["-Zregister-pressure=".len()..];
if file.is_empty() {
return Err(err("-Zregister-pressure= needs a file to write to"));
}
opts.register_pressure = Some(file.to_owned());
}
_ if arg.starts_with("-Z") => {
return Err(err(format!(
"`{arg}` is not an unstable option this compiler has, see \
spec/04-driver-and-cli.md section 4.11 for the ones it does"
)));
}
"-m64" | "-m32" | "-mx32" => {
let want: u32 = match arg {
"-m64" => 64,
_ => 32,
};
let have = rucc_target::TargetInfo::new(opts.target).pointer_width;
if have != want {
return Err(err(format!(
"{arg} asks for a {want} bit target and {} is {have} bit, use \
--target= to name the one you mean",
opts.target
)));
}
}
_ if arg.starts_with("-march=")
|| arg.starts_with("-mtune=")
|| arg.starts_with("-mcpu=") => {}
_ if arg.starts_with("-mabi=") => {
let want = &arg["-mabi=".len()..];
let have = match opts.target.arch {
rucc_target::Arch::X86_64 => "sysv",
rucc_target::Arch::Aarch64 => "lp64",
rucc_target::Arch::Riscv64 => "lp64d",
};
if want != have {
return Err(err(format!(
"{arg}: {} uses the {have} convention and this compiler has no other",
opts.target
)));
}
}
"-mcmodel=small" => {}
_ if arg.starts_with("-mcmodel=") => {
return Err(err(format!(
"{arg}: this compiler emits the small code model and no other, see \
spec/12-targets.md"
)));
}
_ if arg.starts_with("-specs=") => {
return Err(err(
"-specs= is not supported: the parts of it builds rely on are -B, -L, \
-nostdlib, -nostartfiles and -Wl,, see spec/04-driver-and-cli.md \
section 4.4",
));
}
_ if arg.starts_with("-Wa,") || arg.starts_with("-Wp,") => {
return Err(err(format!(
"`{arg}` is an argument for a separate assembler or preprocessor, and both \
are inside this compiler rather than programs it runs"
)));
}
"-Xassembler" | "-Xpreprocessor" => {
return Err(err(format!(
"{arg} hands an argument to a separate assembler or preprocessor, and both \
are inside this compiler rather than programs it runs"
)));
}
_ if arg.starts_with("-W") => {}
"-fno-ident"
| "-fident"
| "-funit-at-a-time"
| "-fno-unit-at-a-time"
| "-shared-libgcc"
| "-static-libgcc" => {}
_ if arg.starts_with('-') && arg.len() > 1 => {
return Err(err(format!("unknown option `{arg}`")));
}
_ => inputs.push(Input { path: arg.to_owned(), forced, library: false }),
}
}
if let Some(named) = fetch {
return fetch_action(&named, offline, &inputs);
}
if let Some(first) = sanitizers.first() {
return Err(err(format!(
"-fsanitize={first}: this compiler has no sanitizer instrumentation, and a build that \
asked for one and got none would run its tests unchecked, see \
spec/04-driver-and-cli.md section 4.7. `-fsafety=detect` is the memory checking this \
compiler does have"
)));
}
link.sysroot = sysroot.clone();
link.cache = Some(cache::dir());
link.pinned = pinned;
if threads {
inputs.push(Input::library("pthread"));
}
if let Some(query) = query {
return Ok(Action::Print(answer(&query, &opts, &link)?));
}
if opts.deps.instead_of_compiling {
opts.emit = EmitKind::Preprocessed;
}
if !nostdinc {
opts.search.push_system(runtime::DIR);
let cross = link::cross_sysroot(opts.target, &link);
let kernel = link::cross_kernel(opts.target, &link);
if cross.is_some() {
let target = pinned.unwrap_or_else(|| opts.target.tuple());
opts.glibc_minor = rucc_sysroot::bundled_glibc_minor(target).map_err(|skew| {
err(format!(
"{skew}; pin a release the tree has, or name a tree that has that one \
with --sysroot"
))
})?;
}
let system =
library::header_dirs(opts.target, sysroot.as_deref(), cross.as_ref(), kernel.as_ref());
if system.is_empty() && sysroot.is_none() {
let tuple = pinned.unwrap_or_else(|| opts.target.tuple());
if let Some(wall) = rucc_sysroot::Wall::of(tuple) {
opts.search.explain_missing_system(wall.no_headers(&tuple.to_canonical_string()));
}
}
if sysroot.is_some() {
notes.extend(glibc::skew(opts.target, pinned, &system));
}
for dir in system {
opts.search.push_system(dir);
}
}
opts.search.remove_duplicates();
if print_config {
return Ok(Action::PrintConfig(Box::new(opts)));
}
if print_pipeline {
return Ok(Action::PrintPipeline(Box::new(opts)));
}
let plan = Plan::new(&opts, &inputs, output.as_deref()).map_err(|e| err(e.message))?;
if print_plan {
return Ok(Action::PrintPlan {
opts: Box::new(opts),
plan: Box::new(plan),
link: Box::new(link),
});
}
Ok(Action::Compile {
opts: Box::new(opts),
plan: Box::new(plan),
link: Box::new(link),
jobs,
verbose,
notes,
})
}
fn fetch_action(named: &str, offline: bool, inputs: &[Input]) -> Result<Action, CliError> {
if offline {
return Err(err(
"--fetch asks for a download and --offline forbids every download, so this command \
line asks for two opposite things. Drop one of them: --offline is how a build says it \
will not reach the network, and --fetch is the only thing in this compiler that does",
));
}
if let Some(first) = inputs.first() {
return Err(err(format!(
"--fetch gets a sysroot and compiles nothing, so `{}` on the same command line is an \
input that nothing would read",
first.path
)));
}
let target: TargetTuple = named
.parse()
.map_err(|why| err(format!("--fetch {named}: {why}, so there is no sysroot to get")))?;
let tuple = target.to_canonical_string();
if let Some(wall) = rucc_sysroot::Wall::of(target) {
return Err(err(format!("--fetch {tuple}: {}", wall.no_fetch(&tuple))));
}
let Some(what) = rucc_sysroot::pinned_for(&tuple) else {
return Err(err(unpinned(&tuple)));
};
Ok(Action::Fetch { what, target, cache: cache::dir() })
}
fn unpinned(tuple: &str) -> String {
let pinned = rucc_sysroot::pinned_targets();
if pinned.is_empty() {
return format!(
"this release pins no sysroot for {tuple}, and it pins none for any target yet. A \
sysroot is built and published by the producer in tamnd/rucc-cross, per \
spec/cross-compile/13-distribution.md section 13.8, and a release of this compiler \
names one by URL and by hash afterwards. Until then, pass --sysroot=<dir> to compile \
against a tree you have already"
);
}
format!(
"this release pins no sysroot for {tuple}. What it pins is {}. Pass --sysroot=<dir> to \
compile against a tree you have already",
pinned.join(", ")
)
}
fn fetch_sysroot(what: &rucc_sysroot::Pinned, target: TargetTuple, cache: &std::path::Path) -> i32 {
let tuple = target.to_canonical_string();
let archive = what.archive_in(cache);
let say = |line: &str| println!("rucc: {tuple}: {line}");
match fetch::fetch(what.url, what.sha256, &archive) {
Ok(fetch::Fetched::AlreadyThere) => {
say(&format!("{} is already here and matches the hash", archive.display()));
}
Ok(fetch::Fetched::Downloaded(by)) => {
say(&format!("downloaded {} with {}", what.url, by.program()));
}
Err(why) => return complain(why),
}
match install::install(&archive, what.sha256, target, cache) {
Ok(done) => {
match &done.before {
install::Before::Nothing => {
say(&format!("{} files installed at {}", done.files, done.root.display()));
}
install::Before::TheSame => {
say(&format!(
"the same sysroot is already at {}, so nothing moved",
done.root.display()
));
}
install::Before::Different(was) => {
say(&format!(
"{} files installed at {}, over a tree whose record digested to {was}",
done.files,
done.root.display()
));
}
}
say(&format!("the record digests to {}", done.digest));
0
}
Err(why) => complain(why),
}
}
fn answer(query: &Query, opts: &Options, link: &LinkOptions) -> Result<String, CliError> {
let found = |name: &str| {
link::find_in_search(link, opts.target, name)
.map_or_else(|| name.to_owned(), |path| path.display().to_string())
};
Ok(match query {
Query::Machine => opts.target.to_string(),
Query::Version => VERSION.to_owned(),
Query::Multiarch => link::multiarch(opts.target),
Query::SearchDirs => {
let here = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(std::path::Path::to_path_buf))
.unwrap_or_default();
let list = |dirs: &[PathBuf]| {
dirs.iter().map(|d| d.display().to_string()).collect::<Vec<_>>().join(":")
};
let libraries = link::search_dirs(link, opts.target);
format!(
"install: {}\nprograms: ={}\nlibraries: ={}",
here.display(),
list(&link.prefixes),
list(&libraries)
)
}
Query::Sysroot => {
sysroot_root(opts, link).map(|root| root.display().to_string()).unwrap_or_default()
}
Query::SysrootProvenance => match sysroot_manifest(opts, link)? {
Some(manifest) => manifest.render().trim_end_matches('\n').to_string(),
None => String::new(),
},
Query::SysrootDigest => match sysroot_manifest(opts, link)? {
Some(manifest) => manifest.digest(),
None => String::new(),
},
Query::FileName(name) => found(name),
Query::Libgcc => found("libgcc.a"),
Query::ProgName(name) => link
.prefixes
.iter()
.map(|dir| dir.join(name))
.find(|path| path.is_file())
.map_or_else(|| name.clone(), |path| path.display().to_string()),
})
}
fn sysroot_root(opts: &Options, link: &LinkOptions) -> Option<PathBuf> {
link.sysroot
.clone()
.or_else(|| link::cross_sysroot(opts.target, link).map(|at| at.root().to_path_buf()))
}
fn sysroot_manifest(opts: &Options, link: &LinkOptions) -> Result<Option<Manifest>, CliError> {
let Some(root) = sysroot_root(opts, link) else {
return Ok(None);
};
let path = Sysroot::at(root, opts.target.tuple()).manifest_path();
match std::fs::read_to_string(&path) {
Ok(text) => Manifest::parse(&text)
.map(Some)
.map_err(|why| err(format!("{}: {why}", path.display()))),
Err(why) if why.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(why) => Err(err(format!("{}: {why}", path.display()))),
}
}
#[must_use]
pub fn print_pipeline(opts: &Options) -> String {
let mut settings = rucc_opt::Options::for_level(opts.opt_level);
settings.toggles.clone_from(&opts.passes);
settings.global_fuel = opts.pass_fuel_global;
for (on, spec) in &opts.pass_gates {
let _ = settings.gates.add(*on, spec);
}
rucc_opt::pipeline::print(&settings)
}
#[must_use]
pub fn print_config(opts: &Options) -> String {
let sess = Session::new(opts.clone());
let t = &sess.target;
let mut out = String::new();
let _ = writeln!(out, "version: {VERSION}");
let _ = writeln!(out, "target: {}", opts.target);
let _ = writeln!(out, "arch: {}", opts.target.arch.as_str());
let _ = writeln!(out, "os: {}", opts.target.os.as_str());
let _ = writeln!(out, "env: {}", opts.target.env.as_str());
let _ = writeln!(out, "object-format: {}", t.object_format.as_str());
let _ = writeln!(out, "pointer-width: {}", t.pointer_width);
let _ = writeln!(out, "long-width: {}", t.long_width);
let _ = writeln!(out, "long-double-width: {}", t.long_double_width);
let _ = writeln!(out, "endian: {}", if t.little_endian { "little" } else { "big" });
let _ = writeln!(out, "char-signed: {}", t.char_is_signed);
let _ = writeln!(out, "va-list: {}", t.va_list.map_or("none", |list| list.as_str()));
let regs: Vec<String> = t
.regs
.classes()
.map(|(class, info)| format!("{} {}", info.name, t.regs.len(class)))
.collect();
let _ = writeln!(
out,
"registers: {}",
if regs.is_empty() { "none".to_string() } else { regs.join(", ") }
);
let _ = writeln!(out, "timing-model: {}", t.timing.map_or("none", |timing| timing.model));
let _ = writeln!(out, "opt-level: {}", sess.opts.opt_level);
let _ = writeln!(out, "safety: {}", sess.opts.safety);
let _ = writeln!(out, "emit: {}", sess.opts.emit.as_str());
let _ = writeln!(out, "debug-info: {}", sess.opts.debug_info);
let _ = writeln!(out, "frame-pointer: {}", sess.opts.frame_pointer);
let _ = writeln!(out, "red-zone: {}", sess.opts.red_zone);
let _ = writeln!(out, "stack-protector: {}", sess.opts.protector);
let _ = writeln!(out, "stack-clash-protection: {}", sess.opts.stack_clash);
let _ = writeln!(out, "cf-protection: {}", sess.opts.control);
let _ = writeln!(out, "patchable-function-entry: {}", sess.opts.patchable);
let _ = writeln!(out, "profile: {}", sess.opts.profile);
let _ = writeln!(out, "profile-hook: {}", sess.opts.hook);
for dir in sess.opts.search.dirs() {
let system = if dir.is_system { " (system)" } else { "" };
let _ = writeln!(out, "include: {}{system}", dir.path.display());
}
out
}
fn deps_target_output<'a>(opts: &Options, plan: &'a Plan) -> Option<&'a str> {
if opts.emit == EmitKind::Preprocessed { None } else { plan.output.as_deref() }
}
fn write_named(path: &str, bytes: &[u8]) -> Result<(), String> {
if path == "-" {
return write_out(&Output::Stdout, bytes);
}
write_out(&Output::File(path.to_owned()), bytes)
}
fn write_deps(
opts: &Options,
plan: &Plan,
job: &Job,
found: &[Dependency],
stderr: &mut impl std::io::Write,
) -> bool {
let targets = if opts.deps.targets.is_empty() {
vec![deps::default_target(&job.input, deps_target_output(opts, plan))]
} else {
opts.deps.targets.clone()
};
let rule = deps::rule(&opts.deps, &targets, &job.input, found);
let wrote = match deps::default_file(&opts.deps, &job.input, plan.output.as_deref()) {
Some(path) => write_named(&path, rule.as_bytes()).and_then(|()| {
if opts.deps.instead_of_compiling { write_out(&job.output, b"") } else { Ok(()) }
}),
None => write_out(&job.output, rule.as_bytes()),
};
if let Err(e) = wrote {
let _ = writeln!(stderr, "rucc: error: {e}");
return false;
}
true
}
fn preprocess_all(opts: &Options, plan: &Plan) -> i32 {
let fs = OsFileSystem::new();
let mut stderr = std::io::stderr().lock();
let mut failed = false;
for job in &plan.jobs {
if !job.phases.first().is_some_and(|p| *p == Phase::Preprocess) {
continue;
}
let started = std::time::Instant::now();
let result = preprocess(opts, &job.input, &fs);
if opts.time {
say_time(&job.input, started.elapsed(), &mut stderr);
}
for message in &result.messages {
let _ = writeln!(stderr, "{message}");
}
if result.failed() {
failed = true;
continue;
}
if opts.deps.emit {
failed |= !write_deps(opts, plan, job, &result.deps, &mut stderr);
if opts.deps.instead_of_compiling {
continue;
}
}
if let Err(e) = write_out(&job.output, result.text.as_bytes()) {
let _ = writeln!(stderr, "rucc: error: {e}");
failed = true;
}
}
i32::from(failed)
}
fn compile_all(opts: &Options, plan: &Plan) -> i32 {
let fs = OsFileSystem::new();
let mut stderr = std::io::stderr().lock();
let mut failed = false;
let (mut remarks, ok) = Remarks::new(opts.opt_info_file.as_ref(), &mut stderr);
failed |= !ok;
let mut fired = Fired::new();
let mut pressure = Pressure::new();
let mut lowerings = Lowerings::new();
for job in &plan.jobs {
if !job.phases.contains(&Phase::Compile) {
continue;
}
let started = std::time::Instant::now();
let result = if job.kind == InputKind::Ir {
compile_ir(opts, &job.input, &fs)
} else {
compile(opts, &job.input, &fs)
};
if opts.time {
say_time(&job.input, started.elapsed(), &mut stderr);
}
fired.merge(&result.fired);
pressure.merge(&result.pressure);
lowerings.merge(&result.lowerings);
failed |= !write_dumps(&job.input, &result.dumps, &mut stderr);
failed |= !remarks.write(&result.remarks, &mut stderr);
for message in &result.messages {
let _ = writeln!(stderr, "{message}");
}
failed |= !write_temps(job, &result.temps, &mut stderr);
if result.failed() {
failed = true;
continue;
}
if opts.deps.emit {
failed |= !write_deps(opts, plan, job, &result.deps, &mut stderr);
}
if let Err(e) = write_out(&job.output, result.artifact.bytes()) {
let _ = writeln!(stderr, "rucc: error: {e}");
failed = true;
}
}
failed |= !write_coverage(opts, &fired, &mut stderr);
failed |= !write_pressure(opts, &pressure, &mut stderr);
failed |= !write_lowering(opts, &lowerings, &mut stderr);
i32::from(failed)
}
struct Scratch {
dir: PathBuf,
}
impl Scratch {
fn new() -> Result<Scratch, String> {
let dir = std::env::temp_dir().join(format!("rucc-{}", std::process::id()));
std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
Ok(Scratch { dir })
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.dir);
}
}
fn link_line(opts: &Options, link: &LinkOptions, job: &LinkJob) -> Result<String, link::Error> {
let linker = link::find(opts.target, link)?;
let args = link::line(opts.target, link, &job.inputs, &job.output)?;
Ok(link::render(&linker, &args))
}
fn link_all(opts: &Options, plan: &Plan, link: &LinkOptions, verbose: bool) -> i32 {
let Some(job) = &plan.link else {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "rucc: error: there is nothing to link");
return 1;
};
if let Err(why) = link::preflight(opts.target, link) {
return complain(why);
}
let linker = match link::find(opts.target, link) {
Ok(linker) => linker,
Err(why) => return complain(why),
};
let scratch = match Scratch::new() {
Ok(scratch) => scratch,
Err(why) => return complain(format!("could not make a place for the object files: {why}")),
};
let fs = OsFileSystem::new();
let mut failed = false;
let mut produced: Vec<String> = Vec::with_capacity(plan.jobs.len());
let mut fired = Fired::new();
let mut pressure = Pressure::new();
let mut lowerings = Lowerings::new();
{
let mut stderr = std::io::stderr().lock();
let (mut remarks, ok) = Remarks::new(opts.opt_info_file.as_ref(), &mut stderr);
failed |= !ok;
for (at, job) in plan.jobs.iter().enumerate() {
let out = match &job.output {
Output::Temporary(hint) => {
scratch.dir.join(format!("{at}-{hint}")).display().to_string()
}
Output::File(path) => path.clone(),
Output::Stdout => continue,
};
produced.push(out.clone());
if !job.phases.contains(&Phase::Compile) {
continue;
}
let started = std::time::Instant::now();
let result = if job.kind == InputKind::Ir {
compile_ir(opts, &job.input, &fs)
} else {
compile(opts, &job.input, &fs)
};
if opts.time {
say_time(&job.input, started.elapsed(), &mut stderr);
}
fired.merge(&result.fired);
pressure.merge(&result.pressure);
lowerings.merge(&result.lowerings);
lowerings.merge(&result.lowerings);
failed |= !write_dumps(&job.input, &result.dumps, &mut stderr);
failed |= !remarks.write(&result.remarks, &mut stderr);
for message in &result.messages {
let _ = writeln!(stderr, "{message}");
}
failed |= !write_temps(job, &result.temps, &mut stderr);
if result.failed() {
failed = true;
continue;
}
if opts.deps.emit {
failed |= !write_deps(opts, plan, job, &result.deps, &mut stderr);
}
if !matches!(result.artifact, Artifact::Object { .. }) {
let _ = writeln!(
stderr,
"rucc: internal error: {}: no object file was produced for the link",
job.input
);
failed = true;
continue;
}
if let Err(e) = std::fs::write(&out, result.artifact.bytes()) {
let _ = writeln!(stderr, "rucc: error: {out}: {e}");
failed = true;
}
}
failed |= !write_coverage(opts, &fired, &mut stderr);
failed |= !write_pressure(opts, &pressure, &mut stderr);
failed |= !write_lowering(opts, &lowerings, &mut stderr);
failed |= !write_lowering(opts, &lowerings, &mut stderr);
}
if failed {
return 1;
}
let mut outputs = produced.into_iter();
let mut items = Vec::with_capacity(job.inputs.len());
for item in &job.inputs {
match item {
link::Item::Library(name) => items.push(link::Item::Library(name.clone())),
link::Item::File(_) => match outputs.next() {
Some(path) => items.push(link::Item::File(path)),
None => return complain("the plan asks the linker for a file nothing produced"),
},
}
}
let args = match link::line(opts.target, link, &items, &job.output) {
Ok(args) => args,
Err(why) => return complain(why),
};
if verbose {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "{}", link::render(&linker, &args));
}
let started = std::time::Instant::now();
let ran = link::run(&linker, &args);
if opts.time {
let mut stderr = std::io::stderr().lock();
say_time(&linker.name, started.elapsed(), &mut stderr);
}
match ran {
Ok(()) => 0,
Err(link::Error::Refused { .. }) => 1,
Err(why) => complain(why),
}
}
fn archive_all(opts: &Options, plan: &Plan) -> i32 {
let Some(job) = &plan.archive else {
return complain("there is nothing to put in an archive");
};
let flavour = match opts.target.os.object_format() {
ObjectFormat::Elf => rucc_archive::Flavour::Gnu,
ObjectFormat::Coff => rucc_archive::Flavour::Coff,
format @ (ObjectFormat::MachO | ObjectFormat::Wasm) => {
return complain(format!(
"there is no archive format for {} objects in this compiler yet",
format.as_str()
));
}
};
let fs = OsFileSystem::new();
let mut failed = false;
let mut members: Vec<rucc_archive::Member> = Vec::with_capacity(plan.jobs.len());
let mut names = job.members.iter();
let mut fired = Fired::new();
let mut pressure = Pressure::new();
let mut lowerings = Lowerings::new();
{
let mut stderr = std::io::stderr().lock();
let (mut remarks, ok) = Remarks::new(opts.opt_info_file.as_ref(), &mut stderr);
failed |= !ok;
for plan_job in &plan.jobs {
let Some(member) = names.next() else {
return complain("the plan asks the archive for a member nothing produced");
};
if !plan_job.phases.contains(&Phase::Compile) {
let _ = writeln!(
&mut stderr,
"rucc: error: {}: this compiler has no assembler for a file of assembly yet, \
so it cannot go into an archive",
plan_job.input
);
failed = true;
continue;
}
let started = std::time::Instant::now();
let result = if plan_job.kind == InputKind::Ir {
compile_ir(opts, &plan_job.input, &fs)
} else {
compile(opts, &plan_job.input, &fs)
};
if opts.time {
say_time(&plan_job.input, started.elapsed(), &mut stderr);
}
fired.merge(&result.fired);
pressure.merge(&result.pressure);
lowerings.merge(&result.lowerings);
lowerings.merge(&result.lowerings);
failed |= !write_dumps(&plan_job.input, &result.dumps, &mut stderr);
failed |= !remarks.write(&result.remarks, &mut stderr);
for message in &result.messages {
let _ = writeln!(stderr, "{message}");
}
failed |= !write_temps(plan_job, &result.temps, &mut stderr);
if result.failed() {
failed = true;
continue;
}
if opts.deps.emit {
failed |= !write_deps(opts, plan, plan_job, &result.deps, &mut stderr);
}
let Artifact::Object { bytes, defines } = result.artifact else {
let _ = writeln!(
stderr,
"rucc: internal error: {}: no object file was produced for the archive",
plan_job.input
);
failed = true;
continue;
};
if let Output::File(path) = &plan_job.output {
if let Err(e) = std::fs::write(path, &bytes) {
let _ = writeln!(stderr, "rucc: error: {path}: {e}");
failed = true;
}
}
members.push(rucc_archive::Member { name: member.clone(), body: bytes, defines });
}
failed |= !write_coverage(opts, &fired, &mut stderr);
failed |= !write_pressure(opts, &pressure, &mut stderr);
failed |= !write_lowering(opts, &lowerings, &mut stderr);
failed |= !write_lowering(opts, &lowerings, &mut stderr);
}
if failed {
return 1;
}
let bytes = match rucc_archive::write(flavour, &members) {
Ok(bytes) => bytes,
Err(why) => return complain(format!("the archive could not be written: {why}")),
};
match std::fs::write(&job.output, &bytes) {
Ok(()) => 0,
Err(e) => complain(format!("{}: {e}", job.output)),
}
}
fn complain(why: impl std::fmt::Display) -> i32 {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "rucc: error: {why}");
1
}
fn write_coverage(opts: &Options, fired: &Fired, stderr: &mut impl std::io::Write) -> bool {
let Some(path) = &opts.rule_coverage else { return true };
let Some(table) = coverage::table(opts.target.arch) else {
let _ = writeln!(
stderr,
"rucc: error: there are no lowering rules for {} yet, so there is no coverage of them \
to report",
opts.target
);
return false;
};
match std::fs::write(path, fired.listing(table)) {
Ok(()) => true,
Err(e) => {
let _ = writeln!(stderr, "rucc: error: {path}: {e}");
false
}
}
}
fn write_pressure(opts: &Options, pressure: &Pressure, stderr: &mut impl std::io::Write) -> bool {
let Some(path) = &opts.register_pressure else { return true };
match std::fs::write(path, pressure.listing()) {
Ok(()) => true,
Err(e) => {
let _ = writeln!(stderr, "rucc: error: {path}: {e}");
false
}
}
}
fn write_lowering(opts: &Options, lowerings: &Lowerings, stderr: &mut impl std::io::Write) -> bool {
let Some(path) = &opts.lowering_dump else { return true };
match std::fs::write(path, lowerings.listing()) {
Ok(()) => true,
Err(e) => {
let _ = writeln!(stderr, "rucc: error: {path}: {e}");
false
}
}
}
struct Remarks {
file: Option<String>,
started: bool,
}
impl Remarks {
fn new(file: Option<&String>, stderr: &mut impl std::io::Write) -> (Self, bool) {
let mut ok = true;
if let Some(path) = file {
if let Err(e) = std::fs::write(path, "") {
let _ = writeln!(stderr, "rucc: error: {path}: {e}");
ok = false;
}
}
(Self { file: file.cloned(), started: false }, ok)
}
fn write(&mut self, text: &str, stderr: &mut impl std::io::Write) -> bool {
if text.is_empty() {
return true;
}
let Some(path) = &self.file else {
let _ = write!(stderr, "{text}");
return true;
};
let opened = std::fs::OpenOptions::new()
.write(true)
.append(self.started)
.truncate(!self.started)
.create(true)
.open(path);
self.started = true;
let result =
opened.and_then(|mut file| std::io::Write::write_all(&mut file, text.as_bytes()));
if let Err(e) = result {
let _ = writeln!(stderr, "rucc: error: {path}: {e}");
return false;
}
true
}
}
fn write_dumps(input: &str, dumps: &[rucc_opt::Dump], stderr: &mut impl std::io::Write) -> bool {
let stem = std::path::Path::new(input)
.file_name()
.map_or_else(|| input.to_owned(), |name| name.to_string_lossy().into_owned());
let mut ok = true;
for dump in dumps {
let path = format!("{stem}.{}.ir", dump.name);
if let Err(e) = std::fs::write(&path, &dump.text) {
let _ = writeln!(stderr, "rucc: error: {path}: {e}");
ok = false;
}
}
ok
}
fn write_temps(job: &Job, temps: &Temps, stderr: &mut impl std::io::Write) -> bool {
let mut ok = true;
let kept = [(job.saved_text(), &temps.preprocessed), (job.saved_asm(), &temps.assembly)];
for (path, text) in kept {
let (Some(path), Some(text)) = (path, text) else { continue };
if let Err(e) = std::fs::write(&path, text) {
let _ = writeln!(stderr, "rucc: error: {path}: {e}");
ok = false;
}
}
ok
}
fn say_time(name: &str, took: std::time::Duration, stderr: &mut impl std::io::Write) {
let _ = writeln!(stderr, "# {name} {:.2} {:.2}", took.as_secs_f64(), 0.0);
}
fn write_out(output: &Output, bytes: &[u8]) -> Result<(), String> {
match output {
Output::Stdout => {
let mut stdout = std::io::stdout().lock();
stdout.write_all(bytes).map_err(|e| format!("writing to standard output: {e}"))
}
Output::File(path) | Output::Temporary(path) => {
std::fs::write(path, bytes).map_err(|e| format!("{path}: {e}"))
}
}
}
pub fn run(args: &[String]) -> i32 {
match parse_args(args) {
Ok(Action::Help) => {
print!("{USAGE}");
0
}
Ok(Action::Version) => {
println!("rucc {VERSION}");
0
}
Ok(Action::Print(line)) => {
println!("{line}");
0
}
Ok(Action::PrintConfig(opts)) => {
print!("{}", print_config(&opts));
0
}
Ok(Action::PrintPipeline(opts)) => {
print!("{}", print_pipeline(&opts));
0
}
Ok(Action::PrintPlan { opts, plan, link }) => {
print!("{}", plan.render());
if let Some(job) = &plan.link {
match link_line(&opts, &link, job) {
Ok(line) => println!("{line}"),
Err(why) => {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "rucc: error: {why}");
return 1;
}
}
}
0
}
Ok(Action::Fetch { what, target, cache }) => fetch_sysroot(what, target, &cache),
Ok(Action::Compile { opts, plan, link, jobs, verbose, notes }) => {
{
let mut stderr = std::io::stderr().lock();
for note in ¬es {
let _ = writeln!(stderr, "rucc: warning: {note}");
}
if verbose {
let _ = write!(stderr, "{}", plan.render());
let _ = writeln!(stderr, "workers: {}", jobs.count());
}
}
if opts.emit == EmitKind::Preprocessed {
return preprocess_all(&opts, &plan);
}
if opts.emit == EmitKind::Archive {
return archive_all(&opts, &plan);
}
if opts.emit != EmitKind::Executable {
return compile_all(&opts, &plan);
}
link_all(&opts, &plan, &link, verbose)
}
Err(e) => {
let mut stderr = std::io::stderr().lock();
let _ = writeln!(stderr, "rucc: error: {e}");
let _ = writeln!(stderr, "rucc: note: run `rucc --help` for usage");
1
}
}
}
#[cfg(test)]
mod tests {
use rucc_session::{
Contract, GnucVersion, IncludeForm, LtoJobs, OptLevel, Partition, Patchable, Visibility,
};
use super::*;
fn args(s: &[&str]) -> Vec<String> {
s.iter().map(|x| (*x).to_owned()).collect()
}
#[test]
fn help_and_version_win_over_everything_else() {
assert_eq!(parse_args(&args(&["-c", "--help", "x.c"])).unwrap(), Action::Help);
assert_eq!(parse_args(&args(&["--version"])).unwrap(), Action::Version);
}
fn compile(s: &[&str]) -> (Box<Options>, Box<Plan>) {
match parse_args(&args(s)).expect("expected a compilation") {
Action::Compile { opts, plan, .. } => (opts, plan),
other => panic!("expected a compilation, got {other:?}"),
}
}
fn linking(s: &[&str]) -> (Box<LinkOptions>, Box<Plan>) {
match parse_args(&args(s)).expect("expected a compilation") {
Action::Compile { link, plan, .. } => (link, plan),
other => panic!("expected a compilation, got {other:?}"),
}
}
fn notes(s: &[&str]) -> Vec<String> {
match parse_args(&args(s)).expect("expected a compilation") {
Action::Compile { notes, .. } => notes,
other => panic!("expected a compilation, got {other:?}"),
}
}
#[test]
fn a_command_line_with_nothing_wrong_with_it_carries_no_notes() {
assert_eq!(notes(&["-c", "a.c"]), Vec::<String>::new());
}
#[test]
fn a_named_tree_that_is_not_on_the_machine_is_not_a_release_mismatch() {
let said =
notes(&["--target=x86_64-linux-gnu.2.28", "--sysroot=/nowhere-at-all", "-c", "a.c"]);
assert_eq!(said, Vec::<String>::new());
}
#[test]
fn collects_inputs_and_flags() {
let (opts, plan) = compile(&["-c", "-O2", "-g", "a.c", "b.c"]);
let paths: Vec<&str> = plan.jobs.iter().map(|j| j.input.as_str()).collect();
assert_eq!(paths, vec!["a.c", "b.c"]);
assert_eq!(opts.opt_level, OptLevel::O2);
assert_eq!(opts.emit, EmitKind::Object);
assert!(opts.debug_info);
}
#[test]
fn an_unstable_option_is_taken_and_one_that_does_not_exist_is_refused() {
let (opts, _) = compile(&["-c", "-Zrule-coverage=/tmp/rules.cov", "a.c"]);
assert_eq!(opts.rule_coverage.as_deref(), Some("/tmp/rules.cov"));
let (plain, _) = compile(&["-c", "a.c"]);
assert_eq!(plain.rule_coverage, None, "nothing is measured unless it was asked for");
assert!(parse_args(&args(&["-Zrule-coverage=", "a.c"])).is_err(), "a file with no name");
let unknown = parse_args(&args(&["-Zwhat", "a.c"])).expect_err("there is no such option");
assert!(unknown.message.contains("4.11"), "{}", unknown.message);
}
#[test]
fn where_the_register_pressure_goes_is_asked_for_the_same_way() {
let (opts, _) = compile(&["-c", "-O2", "-Zregister-pressure=/tmp/spills.txt", "a.c"]);
assert_eq!(opts.register_pressure.as_deref(), Some("/tmp/spills.txt"));
let (plain, _) = compile(&["-c", "a.c"]);
assert_eq!(plain.register_pressure, None, "nothing is measured unless it was asked for");
assert!(parse_args(&args(&["-Zregister-pressure=", "a.c"])).is_err(), "no file named");
}
#[test]
fn where_the_lowering_dump_goes_is_asked_for_the_same_way() {
let (opts, _) = compile(&["-c", "-O2", "-Zlowering=/tmp/lowering.txt", "a.c"]);
assert_eq!(opts.lowering_dump.as_deref(), Some("/tmp/lowering.txt"));
let (plain, _) = compile(&["-c", "a.c"]);
assert_eq!(plain.lowering_dump, None, "nothing is dumped unless it was asked for");
assert!(parse_args(&args(&["-Zlowering=", "a.c"])).is_err(), "no file named");
}
#[test]
fn scheduling_can_be_turned_on_and_off_and_left_to_the_optimization_level() {
let (on, _) = compile(&["-c", "-O0", "-fschedule-insns2", "a.c"]);
assert_eq!(on.schedule_insns, Some(true));
let (off, _) = compile(&["-c", "-O2", "-fno-schedule-insns2", "a.c"]);
assert_eq!(off.schedule_insns, Some(false));
let (quiet, _) = compile(&["-c", "-O2", "a.c"]);
assert_eq!(quiet.schedule_insns, None, "nothing said, so the level decides");
assert!(quiet.opt_level.schedules(), "and at this level the level says yes");
let (none, _) = compile(&["-c", "a.c"]);
assert!(!none.opt_level.schedules(), "at no optimization it says no");
}
#[test]
fn whether_the_timing_model_is_cycle_accurate_can_be_overridden() {
let (yes, _) = compile(&["-c", "-O2", "-Zcycle-accurate-model=yes", "a.c"]);
assert_eq!(yes.cycle_accurate_model, Some(true));
let (no, _) = compile(&["-c", "-O2", "-Zcycle-accurate-model=no", "a.c"]);
assert_eq!(no.cycle_accurate_model, Some(false));
let (plain, _) = compile(&["-c", "-O2", "a.c"]);
assert_eq!(plain.cycle_accurate_model, None, "the target's own answer stands");
let bad = parse_args(&args(&["-Zcycle-accurate-model=maybe", "a.c"]))
.expect_err("it takes yes or no");
assert!(bad.message.contains("yes or no"), "{}", bad.message);
}
#[test]
fn a_bare_dash_o_means_o1_the_way_gcc_reads_it() {
let (opts, _) = compile(&["-O", "a.c"]);
assert_eq!(opts.opt_level, OptLevel::O1);
}
#[test]
fn dash_x_applies_to_later_inputs_only_and_none_stops_it() {
let (_, plan) = compile(&["a.o", "-x", "c", "b.txt", "-x", "none", "c.o"]);
assert_eq!(plan.jobs[0].kind, InputKind::LinkerInput);
assert_eq!(plan.jobs[1].kind, InputKind::C);
assert_eq!(plan.jobs[2].kind, InputKind::LinkerInput);
}
#[test]
fn dash_j_reaches_the_scheduler_and_defaults_to_the_machine() {
let (_, _, jobs) = match parse_args(&args(&["-j4", "a.c"])).unwrap() {
Action::Compile { opts, plan, jobs, .. } => (opts, plan, jobs),
other => panic!("expected a compilation, got {other:?}"),
};
assert_eq!(jobs.count(), 4);
let default = match parse_args(&args(&["a.c"])).unwrap() {
Action::Compile { jobs, .. } => jobs,
other => panic!("expected a compilation, got {other:?}"),
};
assert_eq!(default, Jobs::available());
assert!(parse_args(&args(&["-j0", "a.c"])).is_err());
}
#[test]
fn triple_hash_prints_the_plan_and_runs_nothing() {
let a = parse_args(&args(&["-###", "-c", "a.c"])).unwrap();
let Action::PrintPlan { plan, .. } = a else { panic!("expected a plan dump") };
assert!(plan.render().contains("a.c: preprocess, compile, assemble -> a.o"));
}
#[test]
fn the_flag_that_keeps_the_intermediate_files_has_three_spellings_and_two_meanings() {
assert_eq!(compile(&["-c", "-save-temps", "a.c"]).0.save_temps, SaveTemps::Object);
assert_eq!(compile(&["-c", "-save-temps=obj", "a.c"]).0.save_temps, SaveTemps::Object);
assert_eq!(compile(&["-c", "-save-temps=cwd", "a.c"]).0.save_temps, SaveTemps::Cwd);
assert_eq!(compile(&["-c", "a.c"]).0.save_temps, SaveTemps::No);
let (opts, _) = compile(&["-c", "-save-temps", "-save-temps=cwd", "a.c"]);
assert_eq!(opts.save_temps, SaveTemps::Cwd);
let e = parse_args(&args(&["-c", "-save-temps=nowhere", "a.c"])).unwrap_err();
assert!(e.message.contains("accepted: cwd, obj"), "{}", e.message);
}
#[test]
fn the_flag_that_times_each_step_reaches_the_options_and_changes_nothing_else() {
let (opts, plan) = compile(&["-c", "-time", "a.c"]);
let (plain, without) = compile(&["-c", "a.c"]);
assert!(opts.time);
assert!(!plain.time);
assert_eq!(plan.jobs[0].output, without.jobs[0].output);
}
#[test]
fn dash_x_names_what_it_accepts_when_it_does_not_know_a_language() {
let e = parse_args(&args(&["-x", "fortran", "a.c"])).unwrap_err();
assert!(e.message.contains("assembler-with-cpp"), "{}", e.message);
}
#[test]
fn a_fetch_of_a_target_nothing_is_pinned_for_says_so_rather_than_reaching_the_network() {
let e = parse_args(&args(&["--fetch", "x86_64-linux-musl"])).unwrap_err();
assert!(e.message.contains("pins no sysroot for x86_64-linux-musl"), "{}", e.message);
assert!(e.message.contains("tamnd/rucc-cross"), "{}", e.message);
let joined = parse_args(&args(&["--fetch=x86_64-linux-musl"])).unwrap_err();
assert_eq!(joined, e);
}
#[test]
fn a_fetch_of_a_target_behind_a_licence_wall_says_so_rather_than_saying_not_yet() {
let e = parse_args(&args(&["--fetch", "aarch64-macos"])).unwrap_err();
assert!(e.message.contains("Xcode licence"), "{}", e.message);
assert!(e.message.contains("there never will be"), "{}", e.message);
assert!(!e.message.contains("tamnd/rucc-cross"), "{}", e.message);
let e = parse_args(&args(&["--fetch", "x86_64-windows-msvc"])).unwrap_err();
assert!(e.message.contains("redistributed"), "{}", e.message);
assert!(e.message.contains("mingw-w64"), "{}", e.message);
let e = parse_args(&args(&["--fetch", "x86_64-windows-gnu"])).unwrap_err();
assert!(e.message.contains("pins no sysroot"), "{}", e.message);
}
#[test]
fn an_apple_target_with_no_sdk_anywhere_carries_the_licence_rather_than_a_missing_directory() {
if cfg!(target_os = "macos") || std::env::var_os("SDKROOT").is_some() {
return;
}
let (opts, _) = compile(&["--target=aarch64-macos", "-c", "a.c"]);
let why = opts.search.missing_system().expect("the wall is the reason there are none");
assert!(why.contains("aarch64-macos needs a macOS SDK"), "{why}");
assert!(why.contains("Xcode licence"), "{why}");
assert!(why.contains("-isysroot"), "{why}");
let (opts, _) = compile(&["--target=aarch64-macos", "-nostdinc", "-c", "a.c"]);
assert_eq!(opts.search.missing_system(), None);
let (opts, _) = compile(&["--target=aarch64-macos", "-isysroot", "/opt/sdk", "-c", "a.c"]);
assert_eq!(opts.search.missing_system(), None);
}
#[test]
fn an_msvc_target_with_no_sdk_named_says_which_environment_needs_nothing_installed() {
if std::env::var_os("INCLUDE").is_some() {
return;
}
let (opts, _) = compile(&["--target=x86_64-windows-msvc", "-c", "a.c"]);
let why = opts.search.missing_system().expect("the wall is the reason there are none");
assert!(why.contains("the Windows SDK and its universal CRT"), "{why}");
assert!(why.contains("mingw-w64"), "{why}");
let (opts, _) = compile(&["--target=x86_64-windows-gnu", "-c", "a.c"]);
assert_eq!(opts.search.missing_system(), None);
}
#[test]
fn a_fetch_with_no_target_and_a_fetch_of_a_tuple_that_is_not_one_both_say_which() {
let e = parse_args(&args(&["--fetch"])).unwrap_err();
assert!(e.message.contains("--fetch requires"), "{}", e.message);
let e = parse_args(&args(&["--fetch", "sparc64-solaris-gnu"])).unwrap_err();
assert!(e.message.contains("--fetch sparc64-solaris-gnu"), "{}", e.message);
assert!(e.message.contains("no sysroot to get"), "{}", e.message);
}
#[test]
fn a_fetch_and_offline_together_is_a_refusal_whichever_way_round_they_are_written() {
for line in [
vec!["--offline", "--fetch", "x86_64-linux-musl"],
vec!["--fetch", "x86_64-linux-musl", "--offline"],
] {
let e = parse_args(&args(&line)).unwrap_err();
assert!(e.message.contains("two opposite things"), "{}", e.message);
}
}
#[test]
fn a_fetch_does_not_compile_anything_and_says_so_when_it_is_handed_a_file() {
let e = parse_args(&args(&["--fetch", "x86_64-linux-musl", "a.c"])).unwrap_err();
assert!(e.message.contains("compiles nothing"), "{}", e.message);
assert!(e.message.contains("a.c"), "{}", e.message);
}
#[test]
fn offline_on_a_compilation_is_the_same_compilation() {
let (opts, plan) = compile(&["-c", "--offline", "a.c"]);
let (plain, without) = compile(&["-c", "a.c"]);
assert_eq!(opts.target, plain.target);
assert_eq!(plan.jobs.len(), without.jobs.len());
assert_eq!(plan.jobs[0].output, without.jobs[0].output);
}
#[test]
fn an_unknown_flag_is_an_error_rather_than_a_shrug() {
let e = parse_args(&args(&["-fno-such-thing", "a.c"])).unwrap_err();
assert!(e.message.contains("unknown option"), "{}", e.message);
}
#[test]
fn permissive_reads_in_both_directions_and_the_last_one_wins() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(!opts.permissive, "off unless it is asked for");
let (opts, _) = compile(&["-c", "-fpermissive", "a.c"]);
assert!(opts.permissive);
let (opts, _) = compile(&["-c", "-fpermissive", "-fno-permissive", "a.c"]);
assert!(!opts.permissive);
}
#[test]
fn asking_for_nested_functions_is_told_why_it_is_not_coming() {
let e = parse_args(&args(&["-fnested-functions", "a.c"])).unwrap_err();
assert!(e.message.contains("trampoline"), "{}", e.message);
assert!(parse_args(&args(&["-fno-nested-functions", "a.c"])).is_ok());
}
#[test]
fn the_flag_every_configure_script_writes_is_taken() {
for flag in ["-fPIC", "-fpic", "-fPIE", "-fpie"] {
let (opts, _) = compile(&["-c", flag, "a.c"]);
assert_eq!(opts.emit, EmitKind::Object, "{flag}");
}
}
#[test]
fn a_table_is_written_unless_the_build_says_nothing_will_walk_it() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(opts.unwinds(), "the default is off");
let (opts, _) = compile(&["-c", "-fno-asynchronous-unwind-tables", "a.c"]);
assert!(!opts.unwinds(), "the build was not taken at its word");
let (opts, _) = compile(&[
"-c",
"-fno-asynchronous-unwind-tables",
"-fasynchronous-unwind-tables",
"a.c",
]);
assert!(opts.unwinds(), "the last flag did not win");
let (opts, _) =
compile(&["-c", "-fno-asynchronous-unwind-tables", "-funwind-tables", "a.c"]);
assert!(opts.unwinds(), "the weaker request was dropped");
let (opts, _) = compile(&["-c", "-fno-unwind-tables", "a.c"]);
assert!(opts.unwinds(), "the weaker negative turned off the stronger request");
let (opts, _) =
compile(&["-c", "-fno-unwind-tables", "-fno-asynchronous-unwind-tables", "a.c"]);
assert!(!opts.unwinds(), "both were turned off and one stayed on");
}
#[test]
fn the_flags_that_describe_what_this_compiler_already_does_are_taken() {
for flag in [
"-fno-common",
"-fstrict-aliasing",
"-fno-strict-aliasing",
"-fdelete-null-pointer-checks",
"-fno-delete-null-pointer-checks",
"-frounding-math",
"-fno-rounding-math",
"-fexcess-precision=standard",
"-fexcess-precision=fast",
"-fexcess-precision=16",
"-pipe",
"-fdiagnostics-color",
"-fno-diagnostics-color",
"-fdiagnostics-color=always",
"-fdiagnostics-color=never",
"-fdiagnostics-color=auto",
] {
let (opts, _) = compile(&["-c", flag, "a.c"]);
assert_eq!(opts.emit, EmitKind::Object, "{flag}");
}
}
#[test]
fn whether_an_exception_is_looked_at_is_kept_and_defaults_to_gccs_answer() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(opts.trapping_math, "the default was not gcc's");
let (opts, _) = compile(&["-c", "-fno-trapping-math", "a.c"]);
assert!(!opts.trapping_math);
let (opts, _) = compile(&["-c", "-ftrapping-math", "a.c"]);
assert!(opts.trapping_math, "spelling out the default turned it off");
let (opts, _) = compile(&["-c", "-fno-trapping-math", "-ftrapping-math", "a.c"]);
assert!(opts.trapping_math);
}
#[test]
fn the_flags_that_name_a_pass_of_gccs_own_are_taken_and_dropped() {
for flag in [
"-fno-tree-ccp",
"-fno-tree-dominator-opts",
"-fno-tree-vrp",
"-fno-tree-bit-ccp",
"-fno-tree-coalesce-vars",
"-ftree-vectorize",
"-ftree-loop-distribution",
"-fno-ipa-cp",
"-fipa-pta",
"-fmodulo-sched",
"-fno-vect-cost-model",
"-fvect-cost-model=unlimited",
"-fsimd-cost-model=cheap",
"-fexpensive-optimizations",
"-fno-early-inlining",
"-fno-inline",
"-finline-functions",
"-foptimize-strlen",
"-fno-ira-share-spill-slots",
] {
let (opts, _) = compile(&["-c", flag, "a.c"]);
assert_eq!(opts.emit, EmitKind::Object, "{flag}");
assert!(opts.passes.is_empty(), "{flag} named a pass of gcc's and not one of ours");
}
}
#[test]
fn a_pass_name_in_either_family_is_taken_whether_or_not_it_is_one_gcc_has() {
for flag in ["-ftree-no-such-pass", "-fno-ipa-no-such-pass"] {
let (opts, _) = compile(&["-c", flag, "a.c"]);
assert_eq!(opts.emit, EmitKind::Object, "{flag}");
}
}
#[test]
fn a_pass_name_this_compiler_has_is_still_read_as_a_pass() {
let (opts, _) = compile(&["-c", "-fno-dce", "a.c"]);
assert_eq!(opts.passes, vec![("dce".to_owned(), false)]);
}
#[test]
fn the_gcc_spelling_of_the_unroller_turns_the_unroller_on_and_off() {
let (opts, _) = compile(&["-c", "-funroll-loops", "a.c"]);
assert_eq!(opts.passes, vec![("unroll".to_owned(), true)]);
let (opts, _) = compile(&["-c", "-fno-unroll-loops", "a.c"]);
assert_eq!(opts.passes, vec![("unroll".to_owned(), false)]);
}
#[test]
fn the_input_charset_is_taken_when_it_names_the_one_that_is_read() {
for flag in ["-finput-charset=utf-8", "-finput-charset=UTF-8", "-finput-charset=utf8"] {
let (opts, _) = compile(&["-c", flag, "a.c"]);
assert_eq!(opts.emit, EmitKind::Object, "{flag}");
}
let e = parse_args(&args(&["-c", "-finput-charset=latin1", "a.c"])).unwrap_err();
assert!(e.message.contains("latin1"), "{}", e.message);
assert!(e.message.contains("UTF-8"), "what is read is worth saying: {}", e.message);
}
#[test]
fn the_three_that_change_the_answer_are_refused_and_their_negatives_are_taken() {
for (flag, word) in [
("-ffast-math", "__FAST_MATH__"),
("-fnon-call-exceptions", "landing pad"),
("-finstrument-functions", "__cyg_profile_func_enter"),
] {
let e = parse_args(&args(&["-c", flag, "a.c"])).unwrap_err();
assert!(e.message.contains(word), "{flag}: {}", e.message);
assert!(!e.message.contains("unknown option"), "{flag} deserves a reason");
let off = format!("-fno-{}", flag.trim_start_matches("-f"));
let (opts, _) = compile(&["-c", &off, "a.c"]);
assert_eq!(opts.emit, EmitKind::Object, "{off}");
}
}
#[test]
fn asking_the_linker_to_merge_tentative_definitions_is_told_why_it_is_not_coming() {
let e = parse_args(&args(&["-fcommon", "a.c"])).unwrap_err();
assert!(e.message.contains(".bss"), "{}", e.message);
assert!(e.message.contains("extern"), "the way out is worth saying: {}", e.message);
}
#[test]
fn asking_for_position_dependent_code_is_told_why_it_is_not_coming() {
for flag in ["-fno-pic", "-fno-pie"] {
let e = parse_args(&args(&[flag, "a.c"])).unwrap_err();
assert!(e.message.contains("global offset table"), "{flag}: {}", e.message);
assert!(e.message.contains("-no-pie"), "{flag}: {}", e.message);
}
}
#[test]
fn an_unsupported_target_names_itself() {
let e = parse_args(&args(&["--target=sparc64-linux-gnu", "a.c"])).unwrap_err();
assert!(e.message.contains("sparc64"), "{}", e.message);
}
#[test]
fn no_inputs_is_an_error_but_print_config_needs_none() {
assert!(parse_args(&args(&[])).is_err());
assert!(matches!(parse_args(&args(&["--print-config"])), Ok(Action::PrintConfig(_))));
}
#[test]
fn print_config_reports_the_target_it_was_given_not_the_host() {
let a = parse_args(&args(&["--print-config", "--target=riscv64-linux-musl"])).unwrap();
let Action::PrintConfig(opts) = a else { panic!("expected a configuration dump") };
let text = print_config(&opts);
assert!(text.contains("target: riscv64-unknown-linux-musl"), "{text}");
assert!(text.contains("char-signed: false"), "{text}");
assert!(text.contains("object-format: elf"), "{text}");
assert!(text.contains("va-list: void-pointer"), "{text}");
assert!(text.contains("registers: none"), "{text}");
assert!(text.contains("timing-model: none"), "{text}");
}
#[test]
fn print_config_names_the_model_the_schedule_was_chosen_with() {
let opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
let text = print_config(&opts);
let line = text.lines().find(|l| l.starts_with("timing-model:")).expect("the model");
assert!(line.contains("Skylake"), "{line}");
assert!(line.contains("published"), "a sentence saying where it came from: {line}");
}
#[test]
fn print_config_has_one_key_per_line_and_a_fixed_order() {
let opts = Options::new("x86_64-unknown-linux-gnu".parse().unwrap());
let text = print_config(&opts);
let keys: Vec<&str> =
text.lines().map(|l| l.split(':').next().unwrap_or_default()).collect();
assert_eq!(keys[0], "version");
assert_eq!(keys[1], "target");
assert_eq!(keys.len(), 26);
assert!(text.ends_with('\n'));
}
#[test]
fn the_safety_tier_is_read_off_the_command_line_and_a_wrong_one_is_refused() {
let (opts, _) = compile(&["a.c"]);
assert_eq!(opts.safety, rucc_session::Safety::Off);
for (flag, tier) in [
("-fsafety=detect", rucc_session::Safety::Detect),
("-fsafety=enforce", rucc_session::Safety::Enforce),
("-fsafety=kernel", rucc_session::Safety::Kernel),
("-fsafety=off", rucc_session::Safety::Off),
] {
let (opts, _) = compile(&[flag, "a.c"]);
assert_eq!(opts.safety, tier, "{flag}");
}
let (opts, _) = compile(&["-fsafety=enforce", "-fsafety=off", "a.c"]);
assert_eq!(opts.safety, rucc_session::Safety::Off);
let e = parse_args(&args(&["-fsafety=on", "a.c"])).unwrap_err();
assert!(e.message.contains("is not a safety tier"), "{}", e.message);
assert!(parse_args(&args(&["-fsafety", "a.c"])).is_err());
}
#[test]
fn the_padding_mode_is_read_off_the_command_line_and_a_wrong_one_is_refused() {
let (opts, _) = compile(&["a.c"]);
assert_eq!(opts.padding, rucc_session::Padding::Ignored);
let (opts, _) = compile(&["-fsafety=detect", "-fsafety-init=padding", "a.c"]);
assert_eq!(opts.padding, rucc_session::Padding::Tracked);
let (opts, _) = compile(&["-fsafety-init=padding", "-fsafety-init=nopadding", "a.c"]);
assert_eq!(opts.padding, rucc_session::Padding::Ignored);
let (opts, _) = compile(&["-fsafety-init=padding", "a.c"]);
assert_eq!(opts.safety, rucc_session::Safety::Off);
let e = parse_args(&args(&["-fsafety-init=some", "a.c"])).unwrap_err();
assert!(e.message.contains("is not a padding mode"), "{}", e.message);
}
#[test]
fn whether_a_write_has_to_stay_inside_its_member_is_read_off_the_command_line() {
let (opts, _) = compile(&["a.c"]);
assert_eq!(opts.subobject, rucc_session::Subobject::Off);
let (opts, _) = compile(&["-fsafety=detect", "-fsafety-subobject", "a.c"]);
assert_eq!(opts.subobject, rucc_session::Subobject::Members);
let (opts, _) = compile(&["-fsafety-subobject", "-fno-safety-subobject", "a.c"]);
assert_eq!(opts.subobject, rucc_session::Subobject::Off);
let e = parse_args(&args(&["-fsafety-subobject=strict", "a.c"])).unwrap_err();
assert!(e.message.contains("tamnd/rucc#967"), "{}", e.message);
}
#[test]
fn whether_two_restrict_pointers_may_meet_is_read_off_the_command_line() {
let (opts, _) = compile(&["a.c"]);
assert_eq!(opts.promise, rucc_session::Promise::Off);
let (opts, _) = compile(&["-fsafety=detect", "-fsafety-restrict", "a.c"]);
assert_eq!(opts.promise, rucc_session::Promise::Blocks);
let (opts, _) = compile(&["-fsafety-restrict", "-fno-safety-restrict", "a.c"]);
assert_eq!(opts.promise, rucc_session::Promise::Off);
let (opts, _) = compile(&["-fsafety-restrict", "a.c"]);
assert_eq!(opts.safety, rucc_session::Safety::Off);
let e = parse_args(&args(&["-fsafety-restrict=blocks", "a.c"])).unwrap_err();
assert!(e.message.contains("takes no value"), "{}", e.message);
}
#[test]
fn safety_races_takes_a_mode_and_defaults_to_watching_nothing() {
let (opts, _) = compile(&["a.c"]);
assert_eq!(opts.races, rucc_session::Races::Off);
let (opts, _) = compile(&["-fsafety-races=metadata", "a.c"]);
assert_eq!(opts.races, rucc_session::Races::Metadata);
let (opts, _) = compile(&["-fsafety-races=pointer", "a.c"]);
assert_eq!(opts.races, rucc_session::Races::Pointer);
let (opts, _) = compile(&["-fsafety-races=pointer", "-fno-safety-races", "a.c"]);
assert_eq!(opts.races, rucc_session::Races::Off);
let e = parse_args(&args(&["-fsafety-races=all", "a.c"])).unwrap_err();
assert!(e.message.contains("off, metadata or pointer"), "{}", e.message);
}
#[test]
fn print_pipeline_answers_with_the_passes_the_level_asked_for() {
let a = parse_args(&args(&["--print-pipeline", "-O2"])).unwrap();
let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
let text = print_pipeline(&opts);
assert!(text.starts_with("level: -O2\n"), "{text}");
assert!(text.contains("fold"), "{text}");
let a = parse_args(&args(&["--print-pipeline"])).unwrap();
let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
assert!(print_pipeline(&opts).contains("1: expect,"), "{}", print_pipeline(&opts));
assert!(print_pipeline(&opts).contains("2: simplify-cfg,"), "{}", print_pipeline(&opts));
let a = parse_args(&args(&["--print-pipeline", "-fno-simplify-cfg"])).unwrap();
let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
let text = print_pipeline(&opts);
assert!(text.contains("1: expect,"), "{text}");
assert!(!text.contains("simplify-cfg"), "{text}");
}
#[test]
fn print_pipeline_takes_the_toggles_into_account() {
let a = parse_args(&args(&["--print-pipeline", "-O2", "-fno-fold"])).unwrap();
let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
let text = print_pipeline(&opts);
assert!(!text.contains("fold"), "{text}");
assert!(text.contains("dce"), "{text}");
let mut off = vec!["--print-pipeline".to_owned(), "-O2".to_owned()];
off.extend(rucc_opt::PASSES.iter().map(|p| format!("-fno-{}", p.name())));
let spelled: Vec<&str> = off.iter().map(String::as_str).collect();
let a = parse_args(&args(&spelled)).unwrap();
let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
let text = print_pipeline(&opts);
let left: Vec<&str> =
rucc_opt::PASSES.iter().filter(|p| p.required()).map(|p| p.name()).collect();
assert_eq!(left, vec!["expect"], "{text}");
for (at, name) in left.iter().enumerate() {
assert!(text.contains(&format!("{}: {name},", at + 1)), "{text}");
}
assert!(!text.contains("dce"), "{text}");
}
#[test]
fn print_pipeline_says_when_a_budget_will_stop_the_run_short() {
let a = parse_args(&args(&["--print-pipeline", "-O2"])).unwrap();
let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
assert!(!print_pipeline(&opts).contains("global fuel"));
let a = parse_args(&args(&["--print-pipeline", "-O2", "-fpass-fuel-global=4"])).unwrap();
let Action::PrintPipeline(opts) = a else { panic!("expected a pipeline dump") };
let text = print_pipeline(&opts);
assert!(text.contains("global fuel: 4"), "{text}");
}
#[test]
fn a_pass_is_named_by_dash_f_and_unnamed_by_dash_f_no() {
let (opts, _) = compile(&["-c", "-O0", "-ffold", "-fno-fold", "-ffold", "a.c"]);
assert_eq!(
opts.passes,
[("fold".to_owned(), true), ("fold".to_owned(), false), ("fold".to_owned(), true)]
);
let e = parse_args(&args(&["-fno-such-pass", "a.c"])).unwrap_err();
assert!(e.message.contains("unknown option"), "{}", e.message);
}
#[test]
fn pass_fuel_names_a_pass_and_a_count_and_refuses_anything_else() {
let (opts, _) = compile(&["-c", "-O2", "-fpass-fuel=fold=3", "a.c"]);
assert_eq!(opts.pass_fuel, [("fold".to_owned(), 3)]);
let e = parse_args(&args(&["-fpass-fuel=fold", "a.c"])).unwrap_err();
assert!(e.message.contains("<pass>=<count>"), "{}", e.message);
let e = parse_args(&args(&["-fpass-fuel=nosuch=3", "a.c"])).unwrap_err();
assert!(e.message.contains("--print-pipeline"), "{}", e.message);
let e = parse_args(&args(&["-fpass-fuel=fold=lots", "a.c"])).unwrap_err();
assert!(e.message.contains("not a number"), "{}", e.message);
}
#[test]
fn global_pass_fuel_is_a_count_on_its_own_and_defaults_to_no_limit() {
let (opts, _) = compile(&["-c", "-O2", "a.c"]);
assert_eq!(opts.pass_fuel_global, None);
let (opts, _) = compile(&["-c", "-O2", "-fpass-fuel-global=12", "a.c"]);
assert_eq!(opts.pass_fuel_global, Some(12));
assert!(opts.pass_fuel.is_empty());
let e = parse_args(&args(&["-fpass-fuel-global=lots", "a.c"])).unwrap_err();
assert!(e.message.contains("not a number"), "{}", e.message);
}
#[test]
fn a_gate_names_a_pass_and_optionally_the_functions_it_covers() {
let (opts, _) = compile(&["-c", "-O2", "-fdisable-fold", "-fenable-fold=2-4,main", "a.c"]);
assert_eq!(
opts.pass_gates,
[(false, "fold".to_owned()), (true, "fold=2-4,main".to_owned())],
"the order is what decides, so it has to survive the parse"
);
let e = parse_args(&args(&["-fdisable-nosuch", "a.c"])).unwrap_err();
assert!(e.message.contains("--print-pipeline"), "{}", e.message);
let e = parse_args(&args(&["-fenable-fold=9-2", "a.c"])).unwrap_err();
assert!(e.message.contains("ends before it starts"), "{}", e.message);
let e = parse_args(&args(&["-fdisable-fold=", "a.c"])).unwrap_err();
assert!(e.message.contains("is empty"), "{}", e.message);
}
#[test]
fn the_pipeline_listing_says_which_passes_a_gate_touched() {
let (opts, _) = compile(&["-c", "-O2", "-fdisable-fold=main", "a.c"]);
let text = print_pipeline(&opts);
assert!(text.contains("fold, "), "{text}");
assert!(text.contains("[off for main]"), "{text}");
}
#[test]
fn a_dump_is_checked_when_it_is_asked_for_rather_than_when_it_is_taken() {
let (opts, _) = compile(&["-c", "-O2", "-fdump-ir=all", "-fdump-ir=after-fold", "a.c"]);
assert_eq!(opts.dump_ir, ["all", "after-fold"]);
let e = parse_args(&args(&["-fdump-ir=after-nosuch", "a.c"])).unwrap_err();
assert!(e.message.contains("nosuch"), "{}", e.message);
assert!(parse_args(&args(&["-fdump-ir=sideways-fold", "a.c"])).is_err());
}
#[test]
fn opt_info_takes_kinds_and_a_file_and_refuses_a_kind_it_does_not_have() {
let (opts, _) = compile(&["-c", "-O2", "-fopt-info", "a.c"]);
assert_eq!(opts.opt_info, [""], "a bare flag asks for the rewrites");
assert_eq!(opts.opt_info_file, None, "and goes to standard error");
let (opts, _) = compile(&["-c", "-O2", "-fopt-info-missed-note", "a.c"]);
assert_eq!(opts.opt_info, ["missed-note"]);
let (opts, _) =
compile(&["-c", "-O2", "-fopt-info-missed=one.txt", "-fopt-info-all=two.txt", "a.c"]);
assert_eq!(opts.opt_info, ["missed", "all"]);
assert_eq!(opts.opt_info_file.as_deref(), Some("two.txt"));
let e = parse_args(&args(&["-fopt-info-vectorized", "a.c"])).unwrap_err();
assert!(e.message.contains("vectorized"), "{}", e.message);
assert!(e.message.contains("`missed`"), "{}", e.message);
let e = parse_args(&args(&["-fopt-info-missed=", "a.c"])).unwrap_err();
assert!(e.message.contains("no file"), "{}", e.message);
}
#[test]
fn verify_each_is_unstable_and_off_unless_it_was_asked_for() {
let (opts, _) = compile(&["-c", "-Zverify-each", "a.c"]);
assert!(opts.verify_each);
assert!(!USAGE.contains("verify-each"), "an unstable option stays out of the usage text");
}
#[test]
fn dash_o_needs_an_argument() {
let e = parse_args(&args(&["a.c", "-o"])).unwrap_err();
assert_eq!(e.message, "-o requires an argument");
}
#[test]
fn dash_d_and_dash_u_are_read_joined_or_separated_and_keep_their_order() {
let (opts, _) = compile(&["-DFOO=1", "-D", "BAR", "-UBAZ", "-U", "QUX", "a.c"]);
assert_eq!(opts.defines, ["FOO=1", "BAR"]);
assert_eq!(opts.undefines, ["BAZ", "QUX"]);
}
#[test]
fn the_include_flags_land_on_the_chain_each_one_names() {
let (opts, _) = compile(&[
"-Ii",
"-iquote",
"q",
"-isystem",
"sys",
"-idirafter",
"after",
"--sysroot=/nowhere-at-all",
"a.c",
]);
let dirs: Vec<&str> = opts.search.dirs().iter().filter_map(|d| d.path.to_str()).collect();
assert_eq!(dirs, ["q", "i", "sys", runtime::DIR, "after"]);
assert!(!opts.search.dirs()[1].is_system);
assert!(opts.search.dirs()[2].is_system);
}
#[test]
fn the_librarys_headers_come_after_the_compilers_own_and_go_away_with_them() {
let (opts, _) = compile(&["a.c"]);
let dirs = opts.search.dirs();
let ours = dirs.iter().position(|d| d.path.to_str() == Some(runtime::DIR));
assert_eq!(ours, Some(0), "{dirs:?}");
assert!(dirs[1..].iter().all(|d| d.is_system), "{dirs:?}");
let (bare, _) = compile(&["-nostdinc", "a.c"]);
assert!(bare.search.dirs().is_empty(), "{:?}", bare.search.dirs());
}
#[test]
fn a_sysroot_moves_the_librarys_directories_and_nothing_else() {
let (opts, _) = compile(&["-isystem", "sys", "--sysroot=/nowhere-at-all", "a.c"]);
let dirs: Vec<&str> = opts.search.dirs().iter().filter_map(|d| d.path.to_str()).collect();
assert_eq!(dirs, ["sys", runtime::DIR]);
}
#[test]
fn a_cross_compile_reads_the_targets_own_headers_rather_than_the_ones_next_door() {
let (opts, _) = compile(&["--target=riscv64-linux-musl", "-c", "a.c"]);
let dirs: Vec<&std::path::Path> =
opts.search.dirs().iter().map(|d| d.path.as_path()).collect();
let root = cache::dir().join("sysroots").join("riscv64-linux-musl");
let kernel = cache::dir().join("kernel-headers");
assert_eq!(dirs.len(), 5, "{dirs:?}");
assert_eq!(dirs[0], std::path::Path::new(runtime::DIR));
assert_eq!(dirs[1], root.join("include").join("riscv64"));
assert_eq!(dirs[2], root.join("include").join("generic"));
assert_eq!(dirs[3], kernel.join("riscv"));
assert_eq!(dirs[4], kernel.join("generic"));
}
#[test]
fn a_cross_compile_to_something_that_is_not_linux_reads_no_kernel_headers() {
let (opts, _) = compile(&["--target=x86_64-pc-windows-gnu", "-c", "a.c"]);
let dirs: Vec<&std::path::Path> =
opts.search.dirs().iter().map(|d| d.path.as_path()).collect();
assert_eq!(dirs.len(), 3, "{dirs:?}");
assert!(!dirs.iter().any(|dir| dir.ends_with("kernel-headers")), "{dirs:?}");
}
#[test]
fn the_glibc_version_macro_goes_with_the_bundled_tree_and_with_nothing_else() {
let gnu = format!("--target={}-linux-gnu", cross_arch());
let (bundled, _) = compile(&[&gnu, "-c", "a.c"]);
assert_eq!(bundled.glibc_minor, Some(44));
let pin = format!("{gnu}.2.28");
let (pinned, _) = compile(&[&pin, "-c", "a.c"]);
assert_eq!(pinned.glibc_minor, Some(28));
let (named, _) = compile(&[&gnu, "--sysroot=/nowhere-at-all", "-c", "a.c"]);
assert_eq!(named.glibc_minor, None);
let (none, _) = compile(&[&gnu, "-nostdinc", "-c", "a.c"]);
assert_eq!(none.glibc_minor, None);
let musl = format!("--target={}-linux-musl", cross_arch());
let (musl, _) = compile(&[&musl, "-c", "a.c"]);
assert_eq!(musl.glibc_minor, None);
if let Some(host) = Triple::host() {
let native = format!("--target={}", host.tuple());
let (native, _) = compile(&[&native, "-c", "a.c"]);
assert_eq!(native.glibc_minor, None);
}
}
#[test]
fn a_pinned_release_on_this_machines_own_target_reads_the_bundled_tree() {
let Some(host) = Triple::host() else { return };
if host.env != rucc_target::Env::Gnu {
return;
}
let pin = format!("--target={}.2.28", host.tuple());
let (opts, _) = compile(&[&pin, "-c", "a.c"]);
assert_eq!(opts.glibc_minor, Some(28));
let root = cache::dir().join("sysroots").join(format!("{}.2.28", host.tuple()));
let dirs: Vec<&std::path::Path> =
opts.search.dirs().iter().map(|d| d.path.as_path()).collect();
assert!(dirs.iter().any(|dir| dir.starts_with(&root)), "{dirs:?}");
assert!(!dirs.iter().any(|dir| *dir == std::path::Path::new("/usr/include")), "{dirs:?}");
}
fn cross_arch() -> &'static str {
match Triple::host().map(|host| host.arch) {
Some(rucc_target::Arch::X86_64) => "aarch64",
_ => "x86_64",
}
}
#[test]
fn a_glibc_newer_than_the_bundled_tree_is_refused_by_name() {
let target = format!("--target={}-linux-gnu.2.99", cross_arch());
let message = refused(&[&target, "-c", "a.c"]);
assert!(message.contains("asked for glibc 2.99"), "{message}");
assert!(message.contains("bundled headers are glibc 2.44"), "{message}");
assert!(message.contains("--sysroot"), "{message}");
}
#[test]
fn a_sysroot_the_user_named_is_still_what_a_cross_compile_reads() {
let (opts, _) =
compile(&["--target=riscv64-linux-musl", "--sysroot=/nowhere-at-all", "-c", "a.c"]);
let dirs: Vec<&std::path::Path> =
opts.search.dirs().iter().map(|d| d.path.as_path()).collect();
assert_eq!(dirs, [std::path::Path::new(runtime::DIR)]);
}
#[test]
fn dash_i_dash_moves_the_bracket_directories_into_the_quoted_chain() {
let (opts, _) =
compile(&["-Iinc1", "-iquote", "inc2", "-I-", "-Iinc3", "-nostdinc", "a.c"]);
let dirs: Vec<&str> = opts.search.dirs().iter().filter_map(|d| d.path.to_str()).collect();
assert_eq!(dirs, ["inc1", "inc2", "inc3"]);
assert_eq!(opts.search.start(IncludeForm::Angled), 2);
assert!(!opts.search.searches_current_dir());
}
#[test]
fn the_prefix_flags_stick_what_iprefix_said_on_the_front_of_what_follows_it() {
let (opts, _) = compile(&[
"-iprefix",
"/tools/",
"-iwithprefix",
"late",
"-iwithprefixbefore",
"early",
"-iprefix",
"/other/",
"-iwithprefix",
"last",
"-nostdinc",
"a.c",
]);
let dirs: Vec<&str> = opts.search.dirs().iter().filter_map(|d| d.path.to_str()).collect();
assert_eq!(dirs, ["/tools/early", "/tools/late", "/other/last"]);
assert!(!opts.search.dirs()[0].is_system);
assert!(opts.search.dirs()[1].is_system);
}
#[test]
fn the_files_named_on_the_command_line_keep_their_order_and_which_flag_named_them() {
let (opts, _) =
compile(&["-include", "one.h", "-imacros", "two.h", "-include", "3.h", "a.c"]);
let names: Vec<&str> = opts.preincludes.iter().map(|p| p.name.as_str()).collect();
assert_eq!(names, ["one.h", "two.h", "3.h"]);
assert_eq!(opts.preincludes.iter().filter(|p| p.macros_only).count(), 1);
}
#[test]
fn nostdinc_takes_the_compilers_own_headers_off_the_path() {
let (opts, _) = compile(&["-Ii", "-nostdinc", "a.c"]);
let dirs: Vec<&str> = opts.search.dirs().iter().filter_map(|d| d.path.to_str()).collect();
assert_eq!(dirs, ["i"]);
}
#[test]
fn the_dialect_flags_set_the_language_and_the_extensions_separately() {
let (opts, _) = compile(&["-std=gnu11", "a.c"]);
assert_eq!(opts.std, Std::C11);
assert!(opts.gnu_extensions);
let (opts, _) = compile(&["-std=iso9899:1999", "a.c"]);
assert_eq!(opts.std, Std::C99);
assert!(!opts.gnu_extensions);
let (opts, _) = compile(&["-ansi", "a.c"]);
assert_eq!(opts.std, Std::C89);
assert!(!opts.gnu_extensions);
let e = parse_args(&args(&["-std=c94jr", "a.c"])).unwrap_err();
assert!(e.message.contains("unknown dialect"), "{}", e.message);
}
#[test]
fn the_dump_letters_are_a_family_and_everything_else_beginning_with_d_is_not() {
let (opts, _) = compile(&["-dM", "a.c"]);
assert!(opts.dumps.macros);
let (opts, _) = compile(&["-dDM", "a.c"]);
assert!(opts.dumps.macros);
let (opts, _) = compile(&["-dD", "a.c"]);
assert!(!opts.dumps.macros);
let (opts, _) = compile(&["a.c"]);
assert!(!opts.dumps.any());
assert_eq!(printed(&["-dumpversion", "a.c"]), VERSION);
}
#[test]
fn the_gcc_version_claimed_is_a_flag_and_the_short_spellings_are_the_ones_people_write() {
let (opts, _) = compile(&["a.c"]);
assert_eq!(
opts.gnuc,
GnucVersion { major: 7, minor: 0, patch: 0 },
"the lowest claim a modern glibc gives its own declarations to"
);
let (opts, _) = compile(&["-fgnuc-version=15.1.0", "a.c"]);
assert_eq!(opts.gnuc, GnucVersion { major: 15, minor: 1, patch: 0 });
let (opts, _) = compile(&["-fgnuc-version=15", "a.c"]);
assert_eq!(opts.gnuc, GnucVersion { major: 15, minor: 0, patch: 0 });
let (opts, _) = compile(&["-fgnuc-version=13.2", "a.c"]);
assert_eq!(opts.gnuc, GnucVersion { major: 13, minor: 2, patch: 0 });
let e = parse_args(&args(&["-fgnuc-version=15.x", "a.c"])).unwrap_err();
assert!(e.message.contains("minor that is not a number"), "{}", e.message);
let e = parse_args(&args(&["-fgnuc-version=1.2.3.4", "a.c"])).unwrap_err();
assert!(e.message.contains("more than three"), "{}", e.message);
}
#[test]
fn pedantic_has_two_spellings_and_is_not_the_same_knob_as_the_dialect() {
let (opts, _) = compile(&["-std=c17", "-pedantic", "a.c"]);
assert!(opts.pedantic);
assert_eq!(opts.std, Std::C17);
let (opts, _) = compile(&["-Wpedantic", "a.c"]);
assert!(opts.pedantic);
let (opts, _) = compile(&["-std=c17", "a.c"]);
assert!(!opts.pedantic, "a dialect on its own does not diagnose an extension");
}
#[test]
fn dash_p_and_dash_ffreestanding_reach_the_options() {
let (opts, _) = compile(&["-E", "-P", "-ffreestanding", "a.c"]);
assert!(!opts.line_markers);
assert!(!opts.hosted);
assert_eq!(opts.emit, EmitKind::Preprocessed);
}
#[test]
fn the_builtin_flags_are_read_in_both_directions_and_one_name_at_a_time() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(opts.builtins, "a library name means the library function by default");
assert!(opts.no_builtin.is_empty());
let (opts, _) = compile(&["-c", "-fno-builtin", "a.c"]);
assert!(!opts.builtins);
let (opts, _) = compile(&["-c", "-fno-builtin", "-fbuiltin", "a.c"]);
assert!(opts.builtins, "the last mention decides");
let (opts, _) = compile(&["-c", "-fno-builtin-memcpy", "-fno-builtin-nonesuch", "a.c"]);
assert!(opts.builtins, "one name is not the family");
assert_eq!(opts.no_builtin, vec!["memcpy".to_owned(), "nonesuch".to_owned()]);
}
#[test]
fn visibility_takes_the_four_spellings_gcc_takes_and_refuses_the_rest() {
let (opts, _) = compile(&["-c", "a.c"]);
assert_eq!(opts.visibility, Visibility::Default, "exported unless something says not");
for (written, wanted) in [
("default", Visibility::Default),
("hidden", Visibility::Hidden),
("internal", Visibility::Hidden),
("protected", Visibility::Protected),
] {
let (opts, _) = compile(&["-c", &format!("-fvisibility={written}"), "a.c"]);
assert_eq!(opts.visibility, wanted, "{written}");
}
let (opts, _) = compile(&["-c", "-fvisibility=hidden", "-fvisibility=default", "a.c"]);
assert_eq!(opts.visibility, Visibility::Default, "the last mention decides");
let failed = parse_args(&args(&["-fvisibility=none", "a.c"])).expect_err("refused");
assert!(failed.to_string().contains("is not a visibility"), "{failed}");
}
#[test]
fn how_far_a_multiply_and_an_addition_may_be_fused_is_asked_for() {
let (opts, _) = compile(&["-c", "a.c"]);
assert_eq!(opts.fp_contract, Contract::Off, "a licence nobody granted is not assumed");
for (written, wanted) in
[("off", Contract::Off), ("on", Contract::On), ("fast", Contract::Fast)]
{
let (opts, _) = compile(&["-c", &format!("-ffp-contract={written}"), "a.c"]);
assert_eq!(opts.fp_contract, wanted, "{written}");
}
let (opts, _) = compile(&["-c", "-ffp-contract=fast", "-ffp-contract=off", "a.c"]);
assert_eq!(opts.fp_contract, Contract::Off, "the last mention decides");
for bad in ["-ffp-contract=none", "-ffp-contract=", "-ffp-contract=Fast"] {
let failed = parse_args(&args(&[bad, "a.c"])).expect_err("refused");
assert!(failed.to_string().contains("is not a contraction"), "{bad}: {failed}");
}
let failed = parse_args(&args(&["-fexcess-precision=long", "a.c"])).expect_err("refused");
assert!(failed.to_string().contains("is not an excess precision"), "{failed}");
}
#[test]
fn a_prefix_mapping_flag_goes_on_the_list_its_spelling_names() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(opts.prefix_map.macros.is_empty(), "nothing is rewritten unless it is asked for");
assert!(opts.prefix_map.debug.is_empty(), "nor here");
assert!(opts.prefix_map.profile.is_empty(), "nor here");
let (opts, _) = compile(&["-c", "-fmacro-prefix-map=/build=.", "a.c"]);
assert_eq!(opts.prefix_map.macros.apply("/build/a.c"), "./a.c", "the one it names");
assert!(opts.prefix_map.debug.is_empty(), "and not the two it does not");
let (opts, _) = compile(&["-c", "-fdebug-prefix-map=/build=.", "a.c"]);
assert_eq!(opts.prefix_map.debug.apply("/build/a.c"), "./a.c", "the one it names");
assert!(opts.prefix_map.macros.is_empty(), "and not the two it does not");
let (opts, _) = compile(&["-c", "-fprofile-prefix-map=/build=.", "a.c"]);
assert_eq!(opts.prefix_map.profile.apply("/build/a.c"), "./a.c", "the one it names");
assert!(opts.prefix_map.macros.is_empty(), "and not the two it does not");
let (opts, _) = compile(&["-c", "-ffile-prefix-map=/build=.", "a.c"]);
for list in [&opts.prefix_map.macros, &opts.prefix_map.debug, &opts.prefix_map.profile] {
assert_eq!(list.apply("/build/a.c"), "./a.c", "all three at once");
}
let (opts, _) =
compile(&["-c", "-ffile-prefix-map=/a=one", "-ffile-prefix-map=/b=two", "a.c"]);
assert_eq!(opts.prefix_map.macros.apply("/a/x.c"), "one/x.c", "the earlier one still acts");
assert_eq!(opts.prefix_map.macros.apply("/b/x.c"), "two/x.c", "and so does the later one");
for bad in ["-fmacro-prefix-map=nope", "-ffile-prefix-map=", "-fdebug-prefix-map=/build"] {
let failed = parse_args(&args(&[bad, "a.c"])).expect_err("refused");
assert!(failed.to_string().contains("is not a rewrite for"), "{bad}: {failed}");
}
}
#[test]
fn a_section_per_function_and_a_section_per_variable_are_asked_for_one_at_a_time() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(!opts.function_sections, "one text section unless something says otherwise");
assert!(!opts.data_sections);
let (opts, _) = compile(&["-c", "-ffunction-sections", "a.c"]);
assert!(opts.function_sections);
assert!(!opts.data_sections, "one flag is not the other");
let (opts, _) = compile(&["-c", "-fdata-sections", "a.c"]);
assert!(opts.data_sections);
assert!(!opts.function_sections);
let (opts, _) = compile(&[
"-c",
"-ffunction-sections",
"-fno-function-sections",
"-fdata-sections",
"-fno-data-sections",
"a.c",
]);
assert!(!opts.function_sections, "the last mention decides");
assert!(!opts.data_sections, "the last mention decides");
}
#[test]
fn gnu89_inline_is_off_until_it_is_asked_for_and_the_last_mention_decides() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(!opts.gnu89_inline, "C's reading of inline by default");
let (opts, _) = compile(&["-c", "-fgnu89-inline", "a.c"]);
assert!(opts.gnu89_inline);
let (opts, _) = compile(&["-c", "-fgnu89-inline", "-fno-gnu89-inline", "a.c"]);
assert!(!opts.gnu89_inline, "the last mention decides");
let (opts, _) = compile(&["-c", "-std=c89", "a.c"]);
assert!(!opts.gnu89_inline);
}
#[test]
fn the_two_frame_flags_are_read_in_both_directions() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(!opts.frame_pointer, "gcc omits it above -O0 and so does this");
assert!(opts.red_zone, "the psABI has one and nothing said not to use it");
let (opts, _) = compile(&["-c", "-fno-omit-frame-pointer", "-mno-red-zone", "a.c"]);
assert!(opts.frame_pointer);
assert!(!opts.red_zone);
let (opts, _) = compile(&[
"-c",
"-fno-omit-frame-pointer",
"-fomit-frame-pointer",
"-mno-red-zone",
"-mred-zone",
"a.c",
]);
assert!(!opts.frame_pointer, "the last one wins, as it does in gcc");
assert!(opts.red_zone);
}
#[test]
fn the_stack_protector_is_four_flags_and_the_last_one_wins() {
let (opts, _) = compile(&["-c", "a.c"]);
assert_eq!(opts.protector, Protector::None, "gcc protects nothing unless it was asked");
for (flag, want) in [
("-fstack-protector", Protector::Buffers),
("-fstack-protector-strong", Protector::Strong),
("-fstack-protector-all", Protector::All),
] {
let (opts, _) = compile(&["-c", flag, "a.c"]);
assert_eq!(opts.protector, want, "{flag}");
}
for off in ["-fno-stack-protector", "-fno-stack-protector-strong"] {
let (opts, _) = compile(&["-c", "-fstack-protector-strong", off, "a.c"]);
assert_eq!(opts.protector, Protector::None, "{off}");
}
let (opts, _) = compile(&["-c", "-fno-stack-protector", "-fstack-protector-all", "a.c"]);
assert_eq!(opts.protector, Protector::All, "the last one wins either way round");
}
#[test]
fn taking_a_frame_a_page_at_a_time_is_off_until_it_is_asked_for() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(!opts.stack_clash, "gcc takes a frame in one subtraction unless it was asked");
let (opts, _) = compile(&["-c", "-fstack-clash-protection", "a.c"]);
assert!(opts.stack_clash);
let (opts, _) =
compile(&["-c", "-fstack-clash-protection", "-fno-stack-clash-protection", "a.c"]);
assert!(!opts.stack_clash);
let (opts, _) =
compile(&["-c", "-fno-stack-clash-protection", "-fstack-clash-protection", "a.c"]);
assert!(opts.stack_clash, "the last one wins either way round");
let (opts, _) =
compile(&["-c", "-fstack-clash-protection", "-fstack-protector-strong", "a.c"]);
assert!(opts.stack_clash);
assert_eq!(opts.protector, Protector::Strong);
}
#[test]
fn which_control_flow_edges_are_checked_is_asked_for_by_name() {
let (opts, _) = compile(&["-c", "a.c"]);
assert_eq!(opts.control, Control::None, "gcc's default on the targets this compiler has");
for (arg, want) in [
("-fcf-protection", Control::Full),
("-fcf-protection=full", Control::Full),
("-fcf-protection=branch", Control::Branch),
("-fcf-protection=return", Control::Return),
("-fcf-protection=none", Control::None),
("-fcf-protection=check", Control::Check),
] {
let (opts, _) = compile(&["-c", arg, "a.c"]);
assert_eq!(opts.control, want, "{arg}");
}
let (opts, _) = compile(&["-c", "-fcf-protection=full", "-fno-cf-protection", "a.c"]);
assert_eq!(opts.control, Control::None);
let (opts, _) = compile(&["-c", "-fno-cf-protection", "-fcf-protection=branch", "a.c"]);
assert_eq!(opts.control, Control::Branch, "the last one wins either way round");
}
#[test]
fn the_profiler_and_where_its_hook_goes_are_two_separate_questions() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(!opts.profile);
assert_eq!(opts.hook, Hook::Platform, "neither was named, so the target decides");
for arg in ["-pg", "-p"] {
let (opts, _) = compile(&["-c", arg, "a.c"]);
assert!(opts.profile, "{arg}");
let (link, _) = linking(&[arg, "a.c"]);
assert!(link.profile, "{arg} changes the link as well");
}
for (arg, want) in [("-mfentry", Hook::Early), ("-mno-fentry", Hook::Late)] {
let (opts, _) = compile(&["-c", arg, "a.c"]);
assert_eq!(opts.hook, want, "{arg}");
assert!(!opts.profile, "{arg} asks for no call of its own");
}
let (opts, _) = compile(&["-c", "-mfentry", "-mno-fentry", "-pg", "a.c"]);
assert_eq!(opts.hook, Hook::Late, "the last one wins");
assert!(opts.profile);
}
#[test]
fn the_room_a_patcher_is_promised_is_a_number_of_bytes_and_where_they_go() {
let (opts, _) = compile(&["-c", "a.c"]);
assert_eq!(opts.patchable, Patchable::default());
assert!(!opts.patchable.any(), "nothing is reserved unless it was asked for");
let (opts, _) = compile(&["-c", "-fpatchable-function-entry=16", "a.c"]);
assert_eq!(opts.patchable, Patchable { total: 16, before: 0 });
let (opts, _) = compile(&["-c", "-fpatchable-function-entry=5,3", "a.c"]);
assert_eq!(opts.patchable, Patchable { total: 5, before: 3 });
assert_eq!(opts.patchable.after(), 2);
let (opts, _) = compile(&[
"-c",
"-fpatchable-function-entry=5,3",
"-fpatchable-function-entry=2",
"a.c",
]);
assert_eq!(opts.patchable, Patchable { total: 2, before: 0 });
}
#[test]
fn room_in_front_of_the_label_that_is_more_than_the_room_asked_for_is_refused() {
for arg in ["-fpatchable-function-entry=1,2", "-fpatchable-function-entry=x"] {
let e = parse_args(&args(&["-c", arg, "a.c"])).unwrap_err();
assert!(e.message.contains("is not an amount of room to reserve"), "{}", e.message);
}
}
#[test]
fn what_overflows_rather_than_being_undefined_is_asked_for_two_ways() {
let (opts, _) = compile(&["-c", "a.c"]);
assert_eq!(opts.wrapping, Wrapping::NONE, "nothing wraps unless it was asked for");
let (opts, _) = compile(&["-c", "-fwrapv", "a.c"]);
assert_eq!(opts.wrapping, Wrapping { signed: true, pointer: false, trap: false });
let (opts, _) = compile(&["-c", "-fwrapv-pointer", "a.c"]);
assert_eq!(opts.wrapping, Wrapping { signed: false, pointer: true, trap: false });
let (opts, _) = compile(&["-c", "-fno-strict-overflow", "a.c"]);
assert_eq!(opts.wrapping, Wrapping::ALL);
let (opts, _) = compile(&["-c", "-fwrapv", "-fno-wrapv", "a.c"]);
assert_eq!(opts.wrapping, Wrapping::NONE);
let (opts, _) = compile(&["-c", "-fno-strict-overflow", "-fstrict-overflow", "a.c"]);
assert_eq!(opts.wrapping, Wrapping::NONE);
let (opts, _) = compile(&["-c", "-fno-strict-overflow", "-fno-wrapv-pointer", "a.c"]);
assert_eq!(opts.wrapping, Wrapping { signed: true, pointer: false, trap: false });
}
#[test]
fn a_signed_overflow_that_stops_is_the_other_answer_and_not_a_third_one() {
let (opts, _) = compile(&["-c", "-ftrapv", "a.c"]);
assert_eq!(opts.wrapping, Wrapping { signed: false, pointer: false, trap: true });
let (opts, _) = compile(&["-c", "-fwrapv", "-ftrapv", "a.c"]);
assert_eq!(opts.wrapping, Wrapping { signed: false, pointer: false, trap: true });
let (opts, _) = compile(&["-c", "-ftrapv", "-fwrapv", "a.c"]);
assert_eq!(opts.wrapping, Wrapping { signed: true, pointer: false, trap: false });
let (opts, _) = compile(&["-c", "-ftrapv", "-fno-strict-overflow", "a.c"]);
assert_eq!(opts.wrapping, Wrapping::ALL);
let (opts, _) = compile(&["-c", "-ftrapv", "-fno-trapv", "a.c"]);
assert_eq!(opts.wrapping, Wrapping::NONE);
let (opts, _) = compile(&["-c", "-ftrapv", "-fstrict-overflow", "a.c"]);
assert_eq!(opts.wrapping, Wrapping { signed: false, pointer: false, trap: true });
}
#[test]
fn the_signedness_of_a_plain_char_is_asked_for_in_four_ways() {
let (opts, _) = compile(&["-c", "a.c"]);
assert_eq!(opts.char_signed, None);
for flag in ["-fsigned-char", "-fno-unsigned-char"] {
let (opts, _) = compile(&["-c", flag, "a.c"]);
assert_eq!(opts.char_signed, Some(true), "{flag}");
}
for flag in ["-funsigned-char", "-fno-signed-char"] {
let (opts, _) = compile(&["-c", flag, "a.c"]);
assert_eq!(opts.char_signed, Some(false), "{flag}");
}
let (opts, _) = compile(&["-c", "-funsigned-char", "-fsigned-char", "a.c"]);
assert_eq!(opts.char_signed, Some(true));
let (opts, _) =
compile(&["-c", "--target=aarch64-unknown-linux-gnu", "-fsigned-char", "a.c"]);
assert!(Session::new(*opts).target.char_is_signed);
let (opts, _) = compile(&["-c", "--target=aarch64-unknown-linux-gnu", "a.c"]);
assert!(!Session::new(*opts).target.char_is_signed);
}
#[test]
fn the_smallest_enumeration_is_asked_for_and_taken_back() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(!opts.short_enums);
let (opts, _) = compile(&["-c", "-fshort-enums", "a.c"]);
assert!(opts.short_enums);
let (opts, _) = compile(&["-c", "-fshort-enums", "-fno-short-enums", "a.c"]);
assert!(!opts.short_enums);
let (opts, _) = compile(&["-c", "-fno-short-enums", "-fshort-enums", "a.c"]);
assert!(opts.short_enums);
}
#[test]
fn a_control_flow_protection_nothing_means_is_refused() {
let e = parse_args(&args(&["-c", "-fcf-protection=all", "a.c"])).unwrap_err();
assert!(e.message.contains("is not a control flow protection"), "{}", e.message);
assert!(e.message.contains("full, branch, return, none or check"), "{}", e.message);
}
#[test]
fn the_link_flags_are_collected_apart_from_the_compilation() {
let (link, _) = linking(&[
"-static",
"-nostartfiles",
"-rdynamic",
"-s",
"-fuse-ld=mold",
"-L/opt/lib",
"-B",
"/opt/tools",
"a.c",
]);
assert!(link.is_static);
assert!(link.no_startfiles);
assert!(link.export_dynamic);
assert!(link.strip);
assert_eq!(link.use_ld.as_deref(), Some("mold"));
assert_eq!(link.search, vec![PathBuf::from("/opt/lib")]);
assert_eq!(link.prefixes, vec![PathBuf::from("/opt/tools")]);
}
#[test]
fn a_comma_in_dash_wl_separates_two_arguments() {
let (link, _) = linking(&["-Wl,-rpath,/opt/lib", "-Xlinker", "--as-needed", "a.c"]);
assert_eq!(link.passthrough, vec!["-rpath", "/opt/lib", "--as-needed"]);
}
#[test]
fn a_library_keeps_its_place_between_the_objects() {
let (_, plan) = linking(&["--target=x86_64-unknown-linux-gnu", "a.c", "-lm", "b.c"]);
let link = plan.link.expect("expected a link step");
assert_eq!(
link.inputs,
vec![
link::Item::File("a.o".into()),
link::Item::Library("m".into()),
link::Item::File("b.o".into()),
]
);
assert_eq!(plan.jobs.len(), 2);
}
#[test]
fn a_library_on_a_dash_c_line_is_a_note_rather_than_an_error() {
let (_, plan) = linking(&["-c", "-lm", "a.c"]);
assert!(plan.link.is_none());
assert!(plan.notes.iter().any(|n| n.contains("-lm")), "{:?}", plan.notes);
}
#[test]
fn the_sysroot_reaches_the_linker_as_well_as_the_headers() {
let (link, _) = linking(&["--sysroot=/opt/root", "a.c"]);
assert_eq!(link.sysroot, Some(PathBuf::from("/opt/root")));
}
fn printed(s: &[&str]) -> String {
match parse_args(&args(s)).expect("expected an answer") {
Action::Print(line) => line,
other => panic!("expected an answer, got {other:?}"),
}
}
fn refused(s: &[&str]) -> String {
parse_args(&args(s)).expect_err("expected a refusal").message
}
#[test]
fn a_warning_flag_this_compiler_has_not_heard_of_is_taken_rather_than_refused() {
let (opts, _) = compile(&["-Wall", "-Wextra", "-Wno-format-truncation", "-c", "a.c"]);
assert!(!opts.warnings_are_errors);
assert!(opts.warnings);
let (opts, _) = compile(&["-Werror", "-c", "a.c"]);
assert!(opts.warnings_are_errors);
let (opts, _) = compile(&["-w", "-c", "a.c"]);
assert!(!opts.warnings);
let (opts, _) = compile(&["-pedantic-errors", "-c", "a.c"]);
assert!(opts.pedantic && opts.warnings_are_errors);
}
#[test]
fn an_argument_for_a_separate_tool_is_refused_rather_than_dropped() {
assert!(refused(&["-Wa,--noexecstack", "-c", "a.c"]).contains("separate assembler"));
assert!(refused(&["-Wp,-DX", "-c", "a.c"]).contains("separate assembler"));
assert!(refused(&["-specs=/x", "a.c"]).contains("-specs= is not supported"));
assert!(refused(&["-mcmodel=kernel", "-c", "a.c"]).contains("small code model"));
assert!(refused(&["-gdwarf-4", "-c", "a.c"]).contains("DWARF 5"));
assert!(refused(&["-Ofast", "-c", "a.c"]).contains("fast math"));
let no32 = refused(&["--target=x86_64-unknown-linux-gnu", "-m32", "-c", "a.c"]);
assert!(no32.contains("32 bit target"), "{no32}");
}
#[test]
fn the_shape_of_the_debug_output_is_recorded_even_where_there_is_none_of_it() {
let (opts, _) = compile(&["-c", "a.c"]);
assert_eq!(opts.compress, Compress::None, "uncompressed unless somebody asks");
assert_eq!(compile(&["-gz", "-c", "a.c"]).0.compress, Compress::Zlib);
for (spelling, want) in [
("none", Compress::None),
("zlib", Compress::Zlib),
("zlib-gnu", Compress::ZlibGnu),
("zstd", Compress::Zstd),
] {
let (opts, _) = compile(&[&format!("-gz={spelling}"), "-c", "a.c"]);
assert_eq!(opts.compress, want, "{spelling}");
}
for bad in ["-gz=gzip", "-gz="] {
let failed = refused(&[bad, "-c", "a.c"]);
assert!(failed.contains("is not a way to compress"), "{bad}: {failed}");
}
let (opts, _) = compile(&["-gno-split-dwarf", "-g", "-c", "a.c"]);
assert!(opts.debug_info, "the negative spelling says nothing about how much");
let failed = refused(&["-gsplit-dwarf", "-c", "a.c"]);
assert!(failed.contains(".dwo"), "the refusal names the file it would have written");
}
#[test]
fn the_link_time_family_is_read_and_checked_and_nothing_is_done_about_it() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(!opts.lto.requested, "nothing asks unless the command line does");
let (opts, _) = compile(&["-flto", "-c", "a.c"]);
assert!(opts.lto.requested);
assert_eq!(opts.lto.jobs, LtoJobs::One, "bare -flto is one process, the way gcc reads it");
assert!(!compile(&["-flto", "-fno-lto", "-c", "a.c"]).0.lto.requested);
assert!(compile(&["-fno-lto", "-flto", "-c", "a.c"]).0.lto.requested);
for (spelling, want) in [
("auto", LtoJobs::Auto),
("jobserver", LtoJobs::Jobserver),
("1", LtoJobs::One),
("8", LtoJobs::Count(8)),
] {
let (opts, _) = compile(&[&format!("-flto={spelling}"), "-c", "a.c"]);
assert_eq!(opts.lto.jobs, want, "{spelling}");
assert!(opts.lto.requested, "{spelling} asks for it too");
}
for bad in ["-flto=0", "-flto=thin", "-flto=full", "-flto=-1"] {
let failed = refused(&[bad, "-c", "a.c"]);
assert!(failed.contains("link time jobs"), "{bad}: {failed}");
}
assert_eq!(compile(&["-c", "a.c"]).0.lto.partition, Partition::Balanced, "gcc's default");
for (spelling, want) in [
("balanced", Partition::Balanced),
("1to1", Partition::OneToOne),
("one", Partition::One),
("max", Partition::Max),
("none", Partition::None),
] {
let (opts, _) = compile(&[&format!("-flto-partition={spelling}"), "-c", "a.c"]);
assert_eq!(opts.lto.partition, want, "{spelling}");
}
assert!(refused(&["-flto-partition=big", "-c", "a.c"]).contains("partitioning model"));
assert_eq!(compile(&["-c", "a.c"]).0.lto.compression, None, "whatever it does by default");
assert_eq!(compile(&["-flto-compression-level=0", "-c", "a.c"]).0.lto.compression, Some(0));
let (opts, _) = compile(&["-flto-compression-level=19", "-c", "a.c"]);
assert_eq!(opts.lto.compression, Some(19));
for bad in ["-flto-compression-level=20", "-flto-compression-level=-1"] {
let failed = refused(&[bad, "-c", "a.c"]);
assert!(failed.contains("compression level"), "{bad}: {failed}");
}
for taken in [
"-ffat-lto-objects",
"-fno-fat-lto-objects",
"-fuse-linker-plugin",
"-fno-use-linker-plugin",
] {
let (opts, _) = compile(&[taken, "-c", "a.c"]);
assert!(!opts.lto.requested, "{taken} says nothing about whether to do it");
}
}
#[test]
fn reading_a_profile_is_taken_and_writing_one_is_refused() {
let (opts, _) = compile(&["-c", "a.c"]);
assert!(!opts.profile_data.requested, "nothing asks unless the command line does");
assert_eq!(opts.profile_data.path, None);
let (opts, _) = compile(&["-fprofile-use", "-c", "a.c"]);
assert!(opts.profile_data.requested);
assert_eq!(opts.profile_data.path, None, "beside the object, the way gcc looks");
let (opts, _) = compile(&["-fprofile-use=/counts", "-c", "a.c"]);
assert!(opts.profile_data.requested, "naming a path asks for it too");
assert_eq!(opts.profile_data.path.as_deref(), Some("/counts"));
assert!(
!compile(&["-fprofile-use", "-fno-profile-use", "-c", "a.c"]).0.profile_data.requested
);
assert!(
compile(&["-fno-profile-use", "-fprofile-use", "-c", "a.c"]).0.profile_data.requested
);
let (opts, _) = compile(&[
"-fprofile-dir=/build/profiles",
"-fprofile-abs-path",
"-fprofile-correction",
"-fprofile-partial-training",
"-c",
"a.c",
]);
assert_eq!(opts.profile_data.dir.as_deref(), Some("/build/profiles"));
assert!(opts.profile_data.absolute);
assert!(opts.profile_data.correction);
assert!(opts.profile_data.partial_training);
for writing in [
"-fprofile-generate",
"-fprofile-generate=/build/profiles",
"-fprofile-arcs",
"--coverage",
"-fcondition-coverage",
"-fpath-coverage",
] {
let failed = refused(&[writing, "-c", "a.c"]);
assert!(failed.contains("instrument"), "{writing}: {failed}");
}
assert!(refused(&["-ftest-coverage", "-c", "a.c"]).contains(".gcno"), "it names the file");
for taken in ["-fno-profile-generate", "-fno-profile-arcs", "-fno-test-coverage"] {
let (opts, _) = compile(&[taken, "-c", "a.c"]);
assert!(!opts.profile_data.requested, "{taken} asks for nothing");
}
for taken in [
"-fprofile-update=single",
"-fprofile-update=atomic",
"-fprofile-update=prefer-atomic",
"-fprofile-reproducible=serial",
"-fprofile-reproducible=parallel-runs",
"-fprofile-reproducible=multithreaded",
"-fprofile-values",
"-fno-profile-values",
"-fprofile-info-section",
"-fprofile-filter-files=a.c",
"-fprofile-exclude-files=b.c",
"-fprofile-note=a.gcno",
] {
let (opts, _) = compile(&[taken, "-c", "a.c"]);
assert!(!opts.profile_data.requested, "{taken} says nothing about reading one");
}
assert!(refused(&["-fprofile-update=none", "-c", "a.c"]).contains("update method"));
assert!(refused(&["-fprofile-reproducible=any", "-c", "a.c"]).contains("reproducibility"));
}
#[test]
fn a_sanitizer_that_is_still_asked_for_at_the_end_of_the_line_is_refused_by_name() {
for asked in ["address", "undefined", "thread", "kernel-address", "leak", "memory"] {
let failed = refused(&[&format!("-fsanitize={asked}"), "-c", "a.c"]);
assert!(failed.contains(asked), "the refusal names what was asked for: {failed}");
assert!(failed.contains("-fsafety=detect"), "and the nearest thing: {failed}");
}
let failed = refused(&["-fsanitize=address,undefined", "-c", "a.c"]);
assert!(failed.contains("address"), "{failed}");
for bad in ["-fsanitize=bogus", "-fsanitize=address,bogus", "-fno-sanitize=bogus"] {
let failed = refused(&[bad, "-c", "a.c"]);
assert!(failed.contains("is not a sanitizer"), "{bad}: {failed}");
}
assert!(refused(&["-fsanitize=all", "-c", "a.c"]).contains("only `-fno-sanitize=all`"));
for pair in [
["-fsanitize=address", "-fno-sanitize=address"],
["-fsanitize=address,undefined", "-fno-sanitize=all"],
["-fsanitize=undefined", "-fno-sanitize=undefined"],
] {
let (opts, _) = compile(&[pair[0], pair[1], "-c", "a.c"]);
assert_eq!(opts.safety, rucc_session::Safety::Off, "{pair:?} asked for nothing");
}
assert!(!refused(&["-fno-sanitize=address", "-fsanitize=address", "-c", "a.c"]).is_empty());
for taken in [
"-fsanitize-recover=undefined",
"-fno-sanitize-recover=all",
"-fsanitize-trap=undefined",
"-fno-sanitize-trap=all",
"-fsanitize-undefined-trap-on-error",
"-fsanitize-address-use-after-scope",
"-fno-sanitize-address-use-after-scope",
"-fsanitize-sections=.data",
] {
let (opts, _) = compile(&[taken, "-c", "a.c"]);
assert_eq!(opts.safety, rucc_session::Safety::Off, "{taken} asks for no checking");
}
assert!(refused(&["-fsanitize-recover=bogus", "-c", "a.c"]).contains("is not a sanitizer"));
let failed = refused(&["-fsanitize-coverage=trace-pc", "-c", "a.c"]);
assert!(failed.contains("feedback"), "{failed}");
let failed = refused(&["-fsanitize-coverage=trace-pc-guard", "-c", "a.c"]);
assert!(failed.contains("trace-pc or trace-cmp"), "gcc takes two of them: {failed}");
}
#[test]
fn the_levels_gcc_spells_differently_are_the_levels_they_mean() {
assert_eq!(compile(&["-O", "-c", "a.c"]).0.opt_level, OptLevel::O1);
assert_eq!(compile(&["-Og", "-c", "a.c"]).0.opt_level, OptLevel::O1);
assert_eq!(compile(&["-O2", "-c", "a.c"]).0.opt_level, OptLevel::O2);
}
#[test]
fn the_machine_flags_that_name_what_we_already_do_are_taken_and_the_rest_are_not() {
let line = ["--target=x86_64-unknown-linux-gnu", "-m64", "-march=x86-64-v3"];
let (opts, _) =
compile(&[&line[..], &["-mtune=native", "-mabi=sysv", "-c", "a.c"]].concat());
assert_eq!(opts.target.to_string(), "x86_64-unknown-linux-gnu");
let wrong = refused(&["--target=x86_64-unknown-linux-gnu", "-mabi=ms", "-c", "a.c"]);
assert!(wrong.contains("sysv convention"), "{wrong}");
}
#[test]
fn the_thread_flag_is_a_macro_and_a_library_and_the_library_goes_last() {
let (opts, plan) = compile(&["-pthread", "-c", "a.c"]);
assert!(opts.defines.iter().any(|d| d == "_REENTRANT"));
let names: Vec<&str> = plan.jobs.iter().map(|j| j.input.as_str()).collect();
assert_eq!(names, vec!["a.c"]);
}
#[test]
fn the_questions_a_build_system_asks_before_it_compiles_anything() {
let target = "--target=x86_64-unknown-linux-gnu";
assert_eq!(printed(&[target, "-dumpmachine"]), "x86_64-unknown-linux-gnu");
assert_eq!(printed(&[target, "-dumpversion"]), VERSION);
assert_eq!(printed(&[target, "-dumpfullversion"]), VERSION);
assert_eq!(printed(&[target, "-print-multiarch"]), "x86_64-linux-gnu");
assert_eq!(printed(&[target, "-print-file-name=no-such-library.a"]), "no-such-library.a");
assert_eq!(printed(&[target, "-print-prog-name=ld"]), "ld");
let dirs = printed(&[target, "-print-search-dirs"]);
assert!(dirs.starts_with("install: "), "{dirs}");
assert!(dirs.contains("\nlibraries: ="), "{dirs}");
}
#[test]
fn the_sysroot_in_effect_is_the_one_the_command_line_named_or_the_one_for_the_target() {
assert_eq!(printed(&["--sysroot=/opt/cross", "-print-sysroot"]), "/opt/cross");
let root = cache::dir().join("sysroots").join("riscv64-linux-musl");
assert_eq!(
printed(&["--target=riscv64-linux-musl", "-print-sysroot"]),
root.display().to_string()
);
let host = Triple::host().expect("a host this compiler knows");
assert_eq!(printed(&[&format!("--target={host}"), "-print-sysroot"]), "");
}
#[test]
fn the_provenance_of_a_sysroot_is_the_manifest_it_carries() {
let manifest = "rucc sysroot manifest 3\n\
target\tx86_64-linux-musl\n\
kernel\t6.12\n\
include/generic/stdio.h\tmusl-1.2.5\t\
https://musl.libc.org/releases/musl-1.2.5.tar.gz\t\
0000000000000000000000000000000000000000000000000000000000000000\tmit\t\
bundled\n\
lib/libc.so\tmusl-1.2.5\t\
https://musl.libc.org/releases/musl-1.2.5.tar.gz\t\
1111111111111111111111111111111111111111111111111111111111111111\tmit\t\
generated\n";
let tree = TempTree::new("provenance", &[("manifest", manifest)]);
let sysroot = format!("--sysroot={}", tree.0.display());
assert_eq!(printed(&[&sysroot, "-print-sysroot-provenance"]) + "\n", manifest);
let bare = TempTree::new("provenance-bare", &[]);
assert_eq!(
printed(&[&format!("--sysroot={}", bare.0.display()), "-print-sysroot-provenance"]),
""
);
let host = Triple::host().expect("a host this compiler knows");
assert_eq!(printed(&[&format!("--target={host}"), "-print-sysroot-provenance"]), "");
assert_eq!(printed(&[&sysroot, "--print-sysroot-provenance"]) + "\n", manifest);
assert_eq!(
printed(&[&sysroot, "-print-sysroot-digest"]),
"d705ae6ebeafeb7fda4bd57cecc7882bf49784b17015664a09cfae25a1b2000a"
);
assert_eq!(
printed(&[&sysroot, "--print-sysroot-digest"]),
printed(&[&sysroot, "-print-sysroot-digest"])
);
assert_eq!(
printed(&[&format!("--sysroot={}", bare.0.display()), "-print-sysroot-digest"]),
""
);
assert_eq!(printed(&[&format!("--target={host}"), "-print-sysroot-digest"]), "");
}
#[test]
fn a_manifest_this_build_cannot_read_is_refused_rather_than_printed() {
let tree = TempTree::new(
"provenance-bad",
&[("manifest", "rucc sysroot manifest 3\ntarget\tx86_64-linux-musl\nlib/libc.a\n")],
);
let message =
refused(&[&format!("--sysroot={}", tree.0.display()), "-print-sysroot-provenance"]);
assert!(message.contains("manifest"), "{message}");
assert!(message.contains("1 fields where an input has six"), "{message}");
let digest =
refused(&[&format!("--sysroot={}", tree.0.display()), "-print-sysroot-digest"]);
assert_eq!(digest, message);
}
#[test]
fn the_two_dependency_flags_that_stop_after_the_rule_stop_after_the_rule() {
let (opts, _) = compile(&["-M", "a.c"]);
assert!(opts.deps.emit && opts.deps.instead_of_compiling);
assert!(opts.deps.system_headers, "plain -M lists them");
assert_eq!(opts.emit, EmitKind::Preprocessed);
let (opts, _) = compile(&["-M", "-c", "a.c"]);
assert_eq!(opts.emit, EmitKind::Preprocessed);
let (opts, _) = compile(&["-MM", "a.c"]);
assert!(!opts.deps.system_headers);
}
#[test]
fn the_two_that_end_in_d_leave_the_compilation_alone() {
let (opts, _) = compile(&["-MD", "-c", "a.c"]);
assert!(opts.deps.emit && !opts.deps.instead_of_compiling);
assert!(opts.deps.system_headers);
assert_eq!(opts.emit, EmitKind::Object);
let (opts, _) = compile(&["-MMD", "-c", "a.c"]);
assert!(opts.deps.emit && !opts.deps.instead_of_compiling);
assert!(!opts.deps.system_headers);
}
#[test]
fn nothing_puts_the_system_headers_back_once_a_flag_has_taken_them_out() {
let (opts, _) = compile(&["-MM", "-M", "a.c"]);
assert!(!opts.deps.system_headers);
let (opts, _) = compile(&["-MD", "-MMD", "-c", "a.c"]);
assert!(!opts.deps.system_headers);
let (opts, _) = compile(&["-MMD", "-MD", "-c", "a.c"]);
assert!(!opts.deps.system_headers);
}
#[test]
fn a_target_arrives_escaped_from_one_flag_and_untouched_from_the_other() {
let (opts, _) = compile(&["-MM", "-MT", "a b.o", "-MQ", "a b.o", "a.c"]);
assert_eq!(opts.deps.targets, vec!["a b.o".to_owned(), "a\\ b.o".to_owned()]);
}
#[test]
fn the_rest_of_the_family_is_a_file_and_a_switch() {
let (opts, _) = compile(&["-MM", "-MF", "dep.d", "-MP", "a.c"]);
assert_eq!(opts.deps.file.as_deref(), Some("dep.d"));
assert!(opts.deps.phony);
for flag in ["-MF", "-MT", "-MQ"] {
let e = parse_args(&args(&[flag])).unwrap_err();
assert!(e.message.contains("requires an argument"), "{}", e.message);
}
}
struct TempTree(PathBuf);
impl Drop for TempTree {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
impl TempTree {
fn new(name: &str, files: &[(&str, &str)]) -> TempTree {
let dir = std::env::temp_dir().join(format!("rucc-deps-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("temporary directory should be writable");
for (path, text) in files {
let at = dir.join(path);
if let Some(parent) = at.parent() {
std::fs::create_dir_all(parent).expect("creating a subdirectory should work");
}
std::fs::write(&at, text).expect("writing a temporary file should work");
}
TempTree(dir)
}
fn path(&self, name: &str) -> String {
self.0.join(name).to_string_lossy().into_owned()
}
}
#[test]
fn the_rule_names_what_the_includes_found_and_names_each_of_them_once() {
let tree = TempTree::new(
"found",
&[
("a.c", "#include \"one.h\"\n#include \"two.h\"\nint main(void) { return X; }\n"),
("one.h", "#define X 0\n"),
("two.h", "#include \"one.h\"\n"),
],
);
let out = tree.path("dep.d");
let code = run(&args(&["-MM", "-MF", &out, "-o", &tree.path("a.i"), &tree.path("a.c")]));
assert_eq!(code, 0);
let text = std::fs::read_to_string(&out).expect("the rule should have been written");
let names: Vec<&str> = text.split_whitespace().collect();
assert_eq!(names.first(), Some(&"a.o:"), "{text}");
assert_eq!(names.iter().filter(|n| n.ends_with("one.h")).count(), 1, "{text}");
assert_eq!(names.iter().filter(|n| n.ends_with("two.h")).count(), 1, "{text}");
assert_eq!(std::fs::read(tree.path("a.i")).expect("the output should exist"), b"");
}
#[test]
fn a_header_that_is_only_reached_under_a_guard_is_still_a_dependency() {
let tree = TempTree::new(
"guarded",
&[
("a.c", "#include \"g.h\"\n#include \"g.h\"\nint main(void) { return 0; }\n"),
("g.h", "#ifndef G\n#define G\n#endif\n"),
],
);
let out = tree.path("dep.d");
let code = run(&args(&["-MM", "-MF", &out, "-o", &tree.path("a.i"), &tree.path("a.c")]));
assert_eq!(code, 0);
let text = std::fs::read_to_string(&out).expect("the rule should have been written");
assert_eq!(text.split_whitespace().filter(|n| n.ends_with("g.h")).count(), 1, "{text}");
}
#[test]
fn every_imacros_file_is_read_before_every_include_file_whatever_order_they_were_written() {
let tree = TempTree::new(
"preinclude",
&[
("a.c", "int main(void) { return 0; }\n"),
("i.h", "#ifdef FROM_MACROS\nint saw_it;\n#else\nint missed_it;\n#endif\n"),
("m.h", "#define FROM_MACROS 1\nint macros_text;\n"),
],
);
let out = tree.path("a.i");
let code = run(&args(&[
"-E",
"-include",
&tree.path("i.h"),
"-imacros",
&tree.path("m.h"),
"-o",
&out,
&tree.path("a.c"),
]));
assert_eq!(code, 0);
let text = std::fs::read_to_string(&out).expect("the output should have been written");
assert!(text.contains("saw_it"), "{text}");
assert!(!text.contains("macros_text"), "{text}");
}
#[test]
fn a_file_the_command_line_named_is_a_prerequisite_the_same_as_one_a_directive_named() {
let tree = TempTree::new(
"preinclude-deps",
&[
("a.c", "int main(void) { return 0; }\n"),
("i.h", "int from_include;\n"),
("m.h", "#define M 1\n"),
],
);
let out = tree.path("dep.d");
let code = run(&args(&[
"-MM",
"-MF",
&out,
"-include",
&tree.path("i.h"),
"-imacros",
&tree.path("m.h"),
"-o",
&tree.path("a.i"),
&tree.path("a.c"),
]));
assert_eq!(code, 0);
let text = std::fs::read_to_string(&out).expect("the rule should have been written");
assert!(text.contains("i.h"), "{text}");
assert!(text.contains("m.h"), "{text}");
}
#[test]
fn a_command_line_include_that_is_nowhere_on_the_path_is_an_error_and_not_a_warning() {
let tree = TempTree::new(
"preinclude-missing",
&[("sub/a.c", "int main(void) { return 0; }\n"), ("sub/beside.h", "int x;\n")],
);
let code = run(&args(&["-E", "-include", "beside.h", "-o", "-", &tree.path("sub/a.c")]));
assert_eq!(code, 1);
}
#[test]
fn a_command_line_that_links_names_the_executable_and_not_the_object_it_went_through() {
let (opts, plan) = compile(&["-MD", "sub/a.c", "-o", "prog"]);
assert_eq!(plan.output.as_deref(), Some("prog"));
assert_eq!(deps::default_target("sub/a.c", deps_target_output(&opts, &plan)), "prog");
assert_eq!(
deps::default_file(&opts.deps, "sub/a.c", plan.output.as_deref()).as_deref(),
Some("prog.d")
);
}
#[test]
fn the_plan_keeps_the_output_name_because_the_rule_is_written_from_it() {
let (_, plan) = compile(&["-MMD", "-c", "sub/a.c", "-o", "obj/x.o"]);
assert_eq!(plan.output.as_deref(), Some("obj/x.o"));
let (_, plan) = compile(&["-MMD", "-c", "sub/a.c"]);
assert_eq!(plan.output, None);
}
#[test]
fn usage_fits_on_a_screen() {
assert!(USAGE.lines().count() < 72, "usage text has grown past one screen");
}
}