use std::{
env,
ffi::{CStr, CString},
fs, io,
path::{Component, Path, PathBuf},
str::FromStr,
};
use bpf_linker::{
Cpu, Linker, LinkerInput, LinkerOptions, OptLevel, OutputType,
};
use clap::{
Parser,
builder::{PathBufValueParser, TypedValueParser as _},
error::ErrorKind,
};
use thiserror::Error;
use tracing::{Level, info};
use tracing_subscriber::{EnvFilter, fmt::MakeWriter, prelude::*};
use tracing_tree::HierarchicalLayer;
use sbpf_linker::{
OptimizationConfig, ProgramOptions, SbpfArch, SbpfLinkerError,
link_program,
};
#[derive(Debug, Error)]
enum CliError {
#[error(
"optimization level needs to be between 0-3, s or z (instead was `{0}`)"
)]
InvalidOptimization(String),
#[error(
"unknown emission type: `{0}` - expected one of: `llvm-bc`, `asm`, `llvm-ir`, `obj`"
)]
InvalidOutputType(String),
#[error("unknown architecture: `{0}` - expected one of: `v0`, `v3`")]
InvalidArch(String),
#[error(
"sBPF architecture `v0` only supports CPU architectures `generic`, `v1`, or `v2` (instead was `{0}`)"
)]
UnsupportedV0Cpu(Cpu),
#[error(
"invalid `-bpf-stack-size` value `{0}`; expected a positive value that fits in i32"
)]
InvalidStackFrameSize(String),
#[error(
"`-bpf-stack-size` is missing a value; expected `-bpf-stack-size=<value>`"
)]
MissingStackFrameSize,
#[error("SBPF Linker Error. Error detail: ({0}).")]
SbpfLinkerError(#[from] SbpfLinkerError),
#[error("Program Write Error. Error detail: ({msg}).")]
ProgramWriteError { msg: String },
}
#[derive(Copy, Clone, Debug)]
struct CliOptLevel(OptLevel);
impl FromStr for CliOptLevel {
type Err = CliError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(match s {
"0" => OptLevel::No,
"1" => OptLevel::Less,
"2" => OptLevel::Default,
"3" => OptLevel::Aggressive,
"s" => OptLevel::Size,
"z" => OptLevel::SizeMin,
_ => return Err(CliError::InvalidOptimization(s.to_string())),
}))
}
}
#[derive(Copy, Clone, Debug)]
struct CliOutputType(OutputType);
impl FromStr for CliOutputType {
type Err = CliError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(match s {
"llvm-bc" => OutputType::Bitcode,
"asm" => OutputType::Assembly,
"llvm-ir" => OutputType::LlvmAssembly,
"obj" => OutputType::Object,
_ => return Err(CliError::InvalidOutputType(s.to_string())),
}))
}
}
#[derive(Copy, Clone, Debug)]
struct CliArch(SbpfArch);
impl FromStr for CliArch {
type Err = CliError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(match s {
"v0" => SbpfArch::V0,
"v3" => SbpfArch::V3,
_ => return Err(CliError::InvalidArch(s.to_string())),
}))
}
}
fn parent_and_file_name(p: PathBuf) -> anyhow::Result<(PathBuf, PathBuf)> {
let mut comps = p.components();
let file_name = comps
.next_back()
.map(|p| match p {
Component::Normal(p) => Ok(p),
p => Err(anyhow::anyhow!("unexpected path component {:?}", p)),
})
.transpose()?
.ok_or_else(|| anyhow::anyhow!("unexpected empty path"))?;
let parent = comps.as_path();
Ok((parent.to_path_buf(), Path::new(file_name).to_path_buf()))
}
fn find_solana_compiler_builtins_rlib(
inputs: &[PathBuf],
) -> io::Result<Option<PathBuf>> {
if inputs.iter().any(|input| {
input.file_name().and_then(|name| name.to_str()).is_some_and(
|file_name| {
file_name.starts_with("libsolana_compiler_builtins-")
&& file_name.ends_with(".rlib")
},
)
}) {
return Ok(None);
}
let Some(dep_dir) = inputs.iter().find_map(|input| {
let file_name = input.file_name()?.to_str()?;
if file_name.starts_with("libcompiler_builtins-")
&& file_name.ends_with(".rlib")
{
input.parent()
} else {
None
}
}) else {
return Ok(None);
};
let mut latest = None;
for entry in fs::read_dir(dep_dir)? {
let path = entry?.path();
let Some(file_name) = path.file_name().and_then(|name| name.to_str())
else {
continue;
};
if file_name.starts_with("libsolana_compiler_builtins-")
&& file_name.ends_with(".rlib")
{
let modified = path.metadata()?.modified()?;
match &latest {
Some((latest_modified, _)) if modified <= *latest_modified => {
}
_ => latest = Some((modified, path)),
}
}
}
Ok(latest.map(|(_, path)| path))
}
#[derive(Debug, Parser)]
#[command(version)]
struct CommandLine {
#[clap(long)]
target: Option<CString>,
#[clap(long, default_value = "generic")]
cpu: Cpu,
#[clap(long, default_value = "v2")]
override_cpu_flag: Option<Cpu>,
#[clap(
long,
value_name = "features",
default_value = "",
allow_hyphen_values = true
)]
cpu_features: CString,
#[clap(short, long)]
output: PathBuf,
#[clap(long, default_value = "obj")]
emit: Vec<CliOutputType>,
#[clap(long)]
btf: bool,
#[clap(long)]
allow_bpf_trap: bool,
#[clap(short = 'L', number_of_values = 1)]
_libs: Vec<PathBuf>,
#[clap(short = 'O', default_value = "2")]
optimize: Vec<CliOptLevel>,
#[clap(long, value_name = "path")]
export_symbols: Option<PathBuf>,
#[clap(
long,
value_name = "path",
value_parser = PathBufValueParser::new().try_map(parent_and_file_name),
)]
log_file: Option<(PathBuf, PathBuf)>,
#[clap(long, value_name = "level")]
log_level: Option<Level>,
#[clap(long)]
unroll_loops: bool,
#[clap(long)]
ignore_inline_never: bool,
#[clap(long, value_name = "path")]
dump_module: Option<PathBuf>,
#[clap(long, value_name = "dir")]
dump_cfg_dir: Option<PathBuf>,
#[clap(long, default_value_t = true, action = clap::ArgAction::Set)]
sbpf_optimize: bool,
#[clap(long, default_value = "v3")]
arch: CliArch,
#[clap(long, value_name = "args", use_value_delimiter = true, action = clap::ArgAction::Append)]
llvm_args: Vec<CString>,
#[clap(long, default_value_t = true, hide = true, action = clap::ArgAction::Set)]
disable_expand_memcpy_in_order: bool,
#[clap(long)]
disable_memory_builtins: bool,
#[clap(required = true)]
inputs: Vec<PathBuf>,
#[clap(long, value_name = "symbols", use_value_delimiter = true, action = clap::ArgAction::Append)]
export: Vec<String>,
#[clap(long, action = clap::ArgAction::Set, default_value_t = true)]
fatal_errors: bool,
#[clap(long = "debug", hide = true)]
_debug: bool,
#[clap(long, default_value_t = true, hide = true, action = clap::ArgAction::Set)]
deploy: bool,
}
fn tracing_layer<W>(writer: W) -> HierarchicalLayer<W>
where
W: for<'writer> MakeWriter<'writer> + 'static,
{
const TRACING_IDENT: usize = 2;
HierarchicalLayer::new(TRACING_IDENT)
.with_indent_lines(true)
.with_writer(writer)
}
fn llvm_version() -> (u32, u32, u32) {
let (mut major, mut minor, mut patch) = (0, 0, 0);
unsafe {
bpf_linker::llvm_sys::core::LLVMGetVersion(
&mut major, &mut minor, &mut patch,
);
}
(major, minor, patch)
}
enum StackFrameSizeArg<'a> {
WithValue(&'a str),
MissingValue,
}
fn stack_frame_size_arg(arg: &CStr) -> Option<StackFrameSizeArg<'_>> {
const FLAG: &str = "bpf-stack-size";
let arg = arg.to_str().ok()?;
let arg = arg.strip_prefix("--").or_else(|| arg.strip_prefix('-'))?;
let rest = arg.strip_prefix(FLAG)?;
match rest.strip_prefix('=') {
Some(value) => Some(StackFrameSizeArg::WithValue(value)),
None => rest.is_empty().then_some(StackFrameSizeArg::MissingValue),
}
}
fn stack_frame_size_from_llvm_args(
llvm_args: &[CString],
) -> Result<i32, CliError> {
let value = llvm_args
.iter()
.rev()
.find_map(|arg| match stack_frame_size_arg(arg)? {
StackFrameSizeArg::WithValue(value) => Some(value),
StackFrameSizeArg::MissingValue => None,
})
.expect(
"process_cli_options always supplies `-bpf-stack-size=<value>`",
);
value
.parse::<i32>()
.ok()
.filter(|size| *size > 0)
.ok_or_else(|| CliError::InvalidStackFrameSize(value.to_string()))
}
fn process_cli_options<I>(args: I) -> anyhow::Result<CommandLine>
where
I: Iterator<Item = String>,
{
let cli: CommandLine = match Parser::try_parse_from(args) {
Ok(cli) => cli,
Err(err) => match err.kind() {
ErrorKind::DisplayHelp => {
print!("{err}");
std::process::exit(0);
}
ErrorKind::DisplayVersion => {
print!("{err}");
let (major, minor, patch) = llvm_version();
println!("LLVM {major}.{minor}.{patch}");
std::process::exit(0);
}
_ => return Err(err.into()),
},
};
let mut cpu_features = cli.cpu_features;
let misalignment_bytes = b"allows-misaligned-mem-access";
if !cpu_features
.as_bytes()
.windows(misalignment_bytes.len())
.any(|w| w == misalignment_bytes)
{
let mut bytes = cpu_features.into_bytes();
if !bytes.is_empty() {
bytes.push(b',');
}
bytes.extend_from_slice(b"+allows-misaligned-mem-access");
cpu_features = CString::new(bytes).unwrap();
}
let mut llvm_args = cli.llvm_args;
let mut has_stack_frame_size = false;
for arg in &llvm_args {
match stack_frame_size_arg(arg) {
Some(StackFrameSizeArg::WithValue(_)) => {
has_stack_frame_size = true
}
Some(StackFrameSizeArg::MissingValue) => {
return Err(CliError::MissingStackFrameSize.into());
}
None => {}
}
}
if !has_stack_frame_size {
llvm_args.push(CString::new("-bpf-stack-size=4096").unwrap());
}
let cpu = cli.override_cpu_flag.unwrap();
if matches!(cli.arch.0, SbpfArch::V0)
&& !matches!(cpu, Cpu::Generic | Cpu::V1 | Cpu::V2)
{
return Err(CliError::UnsupportedV0Cpu(cpu).into());
}
Ok(CommandLine {
target: cli.target,
override_cpu_flag: cli.override_cpu_flag,
cpu,
cpu_features,
output: cli.output,
emit: cli.emit,
btf: cli.btf,
allow_bpf_trap: cli.allow_bpf_trap,
_libs: cli._libs,
optimize: cli.optimize,
export_symbols: cli.export_symbols,
log_file: cli.log_file,
log_level: cli.log_level,
unroll_loops: cli.unroll_loops,
ignore_inline_never: cli.ignore_inline_never,
dump_module: cli.dump_module,
dump_cfg_dir: cli.dump_cfg_dir,
sbpf_optimize: cli.sbpf_optimize,
arch: cli.arch,
llvm_args,
disable_expand_memcpy_in_order: cli.disable_expand_memcpy_in_order,
disable_memory_builtins: cli.disable_memory_builtins,
inputs: cli.inputs,
export: cli.export,
fatal_errors: cli.fatal_errors,
_debug: cli._debug,
deploy: cli.deploy,
})
}
fn main() -> anyhow::Result<()> {
let args = env::args().map(|arg| {
if arg == "-flavor" { "--flavor".to_string() } else { arg }
});
let cli = process_cli_options(args)?;
let CommandLine {
cpu,
cpu_features,
target,
output,
btf,
allow_bpf_trap,
export_symbols,
log_file,
log_level,
llvm_args,
unroll_loops,
ignore_inline_never,
dump_module,
dump_cfg_dir,
sbpf_optimize,
arch,
disable_expand_memcpy_in_order,
disable_memory_builtins,
mut inputs,
export,
fatal_errors,
deploy,
..
} = cli;
let _guard = {
let filter = EnvFilter::from_default_env();
let filter = match log_level {
None => filter,
Some(log_level) => filter.add_directive(log_level.into()),
};
let subscriber_registry = tracing_subscriber::registry().with(filter);
match log_file {
Some((parent, file_name)) => {
let file_appender =
tracing_appender::rolling::never(parent, file_name);
let (non_blocking, guard) =
tracing_appender::non_blocking(file_appender);
let subscriber = subscriber_registry
.with(tracing_layer(io::stdout))
.with(tracing_layer(non_blocking));
tracing::subscriber::set_global_default(subscriber)?;
Some(guard)
}
None => {
let subscriber =
subscriber_registry.with(tracing_layer(io::stderr));
tracing::subscriber::set_global_default(subscriber)?;
None
}
}
};
info!("command line: {:?}", env::args().collect::<Vec<_>>().join(" "));
let export_symbols = export_symbols.map(fs::read_to_string).transpose()?;
let export_symbols = export_symbols
.as_deref()
.into_iter()
.flat_map(str::lines)
.chain(export.iter().map(String::as_str));
let output_type = match *cli.emit.as_slice() {
[] => unreachable!("emit has a default value"),
[CliOutputType(output_type), ..] => output_type,
};
let optimize = match *cli.optimize.as_slice() {
[] => unreachable!("optimize has a default value"),
[.., CliOptLevel(optimize)] => optimize,
};
let stack_frame_size = stack_frame_size_from_llvm_args(&llvm_args)?;
let mut linker = Linker::new(LinkerOptions {
target,
cpu,
cpu_features,
optimize,
unroll_loops,
ignore_inline_never,
llvm_args,
disable_expand_memcpy_in_order,
disable_memory_builtins,
btf,
allow_bpf_trap,
});
if let Some(path) = dump_module {
linker.set_dump_module_path(path);
}
if let Some(solana_compiler_builtins) =
find_solana_compiler_builtins_rlib(&inputs)?
{
inputs.push(solana_compiler_builtins);
}
let inputs =
inputs.iter().map(|p| LinkerInput::new_from_file(p.as_path()));
linker.link_to_file(inputs, &output, output_type, export_symbols)?;
print!("{:?}", output);
if fatal_errors && linker.has_errors() {
return Err(anyhow::anyhow!(
"LLVM issued diagnostic with error severity"
));
}
let program = std::fs::read(&output).unwrap();
let sbpf_optimization = if sbpf_optimize {
match dump_cfg_dir {
Some(dir) => OptimizationConfig::enabled().with_cfg_dump_dir(dir),
None => OptimizationConfig::enabled(),
}
} else {
OptimizationConfig::disabled()
};
let bytecode = link_program(
&program,
ProgramOptions::new(sbpf_optimization, arch.0, stack_frame_size),
)?;
let src_name = std::path::Path::new(&output)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("main");
let output_path = std::path::Path::new(&output)
.parent()
.unwrap_or_else(|| std::path::Path::new("."))
.join(format!("{src_name}.so"));
std::fs::write(&output_path, &bytecode)
.map_err(|e| CliError::ProgramWriteError { msg: e.to_string() })?;
if deploy {
let final_object = src_name.strip_prefix("lib").unwrap_or(src_name);
let deploy_path = PathBuf::from("target").join("deploy");
std::fs::create_dir_all(&deploy_path).map_err(|e| {
CliError::ProgramWriteError {
msg: format!("failed to create deploy directory: {e}"),
}
})?;
let deploy_file = deploy_path.join(format!("{final_object}.so"));
std::fs::write(&deploy_file, &bytecode).map_err(|e| {
CliError::ProgramWriteError {
msg: format!("failed to write deploy artifact: {e}"),
}
})?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_export_input_args() {
let args = [
"sbpf-linker",
"--export",
"foo",
"--export",
"bar",
"symbols.o",
"rcgu.o",
"-L",
"target/debug/deps",
"-L",
"target/debug",
"-L",
"/home/foo/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib",
"-o",
"/tmp/bin.s",
"--target=bpf",
"--emit=asm",
]
.into_iter()
.map(|s| s.to_string());
let CommandLine {
cpu,
disable_expand_memcpy_in_order,
deploy,
cpu_features,
llvm_args,
sbpf_optimize,
arch,
..
} = process_cli_options(args).unwrap();
assert!(matches!(cpu, Cpu::V2));
assert!(disable_expand_memcpy_in_order);
assert!(deploy);
assert!(sbpf_optimize);
assert!(matches!(arch.0, SbpfArch::V3));
assert_eq!(cpu_features.to_bytes(), b"+allows-misaligned-mem-access");
assert!(
llvm_args
.iter()
.any(|a| a.to_str().unwrap() == "-bpf-stack-size=4096")
);
}
#[test]
fn test_explicit_overrides_of_default_flags() {
let args = [
"sbpf-linker",
"input.o",
"-o",
"/tmp/bin.so",
"--override-cpu-flag=v1",
"--emit=llvm-ir",
"--deploy=false",
"--fatal-errors=false",
"--disable-expand-memcpy-in-order=false",
"--sbpf-optimize=false",
"--arch=v0",
]
.into_iter()
.map(|s| s.to_string());
let CommandLine {
cpu,
emit,
deploy,
fatal_errors,
disable_expand_memcpy_in_order,
sbpf_optimize,
arch,
output,
..
} = process_cli_options(args).unwrap();
assert_eq!(emit.len(), 1);
assert!(matches!(emit[0], CliOutputType(OutputType::LlvmAssembly)));
assert!(!deploy);
assert!(!fatal_errors);
assert!(!disable_expand_memcpy_in_order);
assert!(!sbpf_optimize);
assert!(matches!(cpu, Cpu::V1));
assert!(matches!(arch.0, SbpfArch::V0));
assert_eq!(output, PathBuf::from("/tmp/bin.so"));
}
#[test]
fn test_boolean_and_optional_flags() {
let args = [
"sbpf-linker",
"input.o",
"-o",
"/tmp/bin.o",
"--target=bpfel-unknown-none",
"--btf",
"--allow-bpf-trap",
"--unroll-loops",
"--override-cpu-flag=v1",
"--ignore-inline-never",
"--disable-memory-builtins",
"--log-level=debug",
"--export-symbols=/tmp/exports.txt",
"--dump-module=/tmp/module.ll",
]
.into_iter()
.map(|s| s.to_string());
let CommandLine {
cpu,
target,
btf,
allow_bpf_trap,
unroll_loops,
ignore_inline_never,
disable_memory_builtins,
log_level,
export_symbols,
dump_module,
inputs,
..
} = process_cli_options(args).unwrap();
assert_eq!(
target.as_deref().map(|t| t.to_bytes()),
Some(b"bpfel-unknown-none".as_slice())
);
assert!(btf);
assert!(allow_bpf_trap);
assert!(unroll_loops);
assert!(matches!(cpu, Cpu::V1));
assert!(ignore_inline_never);
assert!(disable_memory_builtins);
assert_eq!(log_level, Some(Level::DEBUG));
assert_eq!(export_symbols, Some(PathBuf::from("/tmp/exports.txt")));
assert_eq!(dump_module, Some(PathBuf::from("/tmp/module.ll")));
assert_eq!(inputs, vec![PathBuf::from("input.o")]);
}
#[test]
fn test_misalignment_feature_not_duplicated_when_already_present() {
let args = [
"sbpf-linker",
"input.o",
"-o",
"/tmp/bin.o",
"--cpu-features=-allows-misaligned-mem-access,+alu32",
]
.into_iter()
.map(|s| s.to_string());
let CommandLine { cpu_features, .. } =
process_cli_options(args).unwrap();
assert_eq!(
cpu_features.to_bytes(),
b"-allows-misaligned-mem-access,+alu32"
);
}
#[test]
fn test_cpu_features_accepts_split_disabled_feature() {
let args = [
"sbpf-linker",
"input.o",
"-o",
"/tmp/bin.o",
"--cpu-features",
"-alu32",
]
.into_iter()
.map(|s| s.to_string());
let CommandLine { cpu_features, .. } =
process_cli_options(args).unwrap();
assert_eq!(
cpu_features.to_bytes(),
b"-alu32,+allows-misaligned-mem-access"
);
}
#[test]
fn test_override_cpu() {
let args = [
"sbpf-linker",
"input.o",
"-o",
"/tmp/bin.o",
"--cpu=v1",
"--override-cpu-flag=v3",
]
.into_iter()
.map(|s| s.to_string());
let CommandLine { cpu, .. } = process_cli_options(args).unwrap();
assert!(matches!(cpu, Cpu::V3));
}
#[test]
fn test_cpu_flag_is_ignored_without_override() {
let args = ["sbpf-linker", "input.o", "-o", "/tmp/bin.o", "--cpu=v3"]
.into_iter()
.map(|s| s.to_string());
let CommandLine { cpu, .. } = process_cli_options(args).unwrap();
assert!(matches!(cpu, Cpu::V2));
}
#[test]
fn test_bpf_stack_size_rejects_a_missing_value() {
for flag in ["-bpf-stack-size", "--bpf-stack-size"] {
let args = vec![
"sbpf-linker".to_string(),
"input.o".to_string(),
"-o".to_string(),
"/tmp/bin.o".to_string(),
format!("--llvm-args={flag}"),
]
.into_iter();
let error = process_cli_options(args).unwrap_err();
assert_eq!(
error.to_string(),
"`-bpf-stack-size` is missing a value; expected `-bpf-stack-size=<value>`"
);
}
}
#[test]
fn test_bpf_stack_size_value_is_not_overridden_by_the_default() {
for flag in ["-bpf-stack-size=8192", "--bpf-stack-size=8192"] {
let args = vec![
"sbpf-linker".to_string(),
"input.o".to_string(),
"-o".to_string(),
"/tmp/bin.o".to_string(),
format!("--llvm-args={flag}"),
]
.into_iter();
let CommandLine { llvm_args, .. } =
process_cli_options(args).unwrap();
assert_eq!(
llvm_args
.iter()
.map(|arg| arg.to_str().unwrap())
.collect::<Vec<_>>(),
[flag]
);
assert_eq!(
stack_frame_size_from_llvm_args(&llvm_args).unwrap(),
8192
);
}
}
#[test]
fn test_sbpf_v0_rejects_unsupported_cpu_arches() {
for unsupported_cpu in ["probe", "v3"] {
let args = vec![
"sbpf-linker".to_string(),
"input.o".to_string(),
"-o".to_string(),
"/tmp/bin.o".to_string(),
"--arch=v0".to_string(),
format!("--override-cpu-flag={unsupported_cpu}"),
]
.into_iter();
let error = process_cli_options(args).unwrap_err();
assert!(error.to_string().contains(
"sBPF architecture `v0` only supports CPU architectures"
));
}
}
}