Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/commands/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,37 @@ pub fn build(args: BuildArgs) -> Result<()> {

// Create necessary directories
create_dir_all(deploy)?;
// Expand .include "file" directives by inlining file contents (relative to source dir)
fn expand_includes(source: &str, base_dir: &Path) -> Result<String, Error> {
let mut result = String::new();
for line in source.lines() {
let trimmed = line.trim();
if let Some(rest) = trimmed.strip_prefix(".include") {
let rest = rest.trim();
if let Some(path) = rest.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
let include_path = base_dir.join(path);
let included = fs::read_to_string(&include_path).map_err(|e| {
Error::msg(format!("Failed to read include '{}': {}", path, e))
})?;
let include_base = include_path.parent().unwrap_or(base_dir);
result.push_str(&expand_includes(&included, include_base)?);
} else {
result.push_str(line);
result.push('\n');
}
} else {
result.push_str(line);
result.push('\n');
}
}
Ok(result)
}

// Function to compile assembly
fn compile_assembly(src: &str, deploy: &str, debug: bool, arch: SbpfArch) -> Result<()> {
let source_code = std::fs::read_to_string(src).unwrap();
let base_dir = Path::new(src).parent().unwrap_or(Path::new("."));
let source_code = expand_includes(&source_code, base_dir)?;
let file = SimpleFile::new(src.to_string(), source_code.clone());

// Build assembler options
Expand Down
144 changes: 144 additions & 0 deletions tests/test_include.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
mod utils;

use {
std::process::Command,
utils::{
TestEnv, init_project, run_build, update_assembly_file, verify_project_structure,
verify_so_files, write_include_file,
},
};

#[test]
fn test_include_directive() {
let env = TestEnv::new("include_test");

init_project(&env, "include_test");
verify_project_structure(&env, "include_test");

// Write included file with custom_log logic
write_include_file(
&env,
"include_test",
"log.s",
r#".global custom_log
custom_log:
lddw r1, message
lddw r2, 14
call sol_log_
exit

.rodata
message: .ascii "Hello, Solana!"
"#,
);

// Main file uses .include
update_assembly_file(
&env,
"include_test",
r#".globl entrypoint
.include "log.s"
.text
entrypoint:
call custom_log
exit
"#,
);

run_build(&env);
verify_so_files(&env);

env.cleanup();
}

#[test]
fn test_include_nested() {
let env = TestEnv::new("include_nested");

init_project(&env, "include_nested");
verify_project_structure(&env, "include_nested");

// Innermost: just the log logic
write_include_file(
&env,
"include_nested",
"log_impl.s",
r#".global custom_log
custom_log:
lddw r1, message
lddw r2, 14
call sol_log_
exit

.rodata
message: .ascii "Nested!"
"#,
);

// Middle: includes log_impl
write_include_file(
&env,
"include_nested",
"log.s",
r#".include "log_impl.s"
"#,
);

// Main: includes log.s
update_assembly_file(
&env,
"include_nested",
r#".globl entrypoint
.include "log.s"
.text
entrypoint:
call custom_log
exit
"#,
);

run_build(&env);
verify_so_files(&env);

env.cleanup();
}

#[test]
fn test_include_missing_file_fails() {
let env = TestEnv::new("include_missing");

init_project(&env, "include_missing");
verify_project_structure(&env, "include_missing");

// Main file references non-existent include
update_assembly_file(
&env,
"include_missing",
r#".globl entrypoint
.include "nonexistent.s"
.text
entrypoint:
exit
"#,
);

let output = Command::new(&env.sbpf_bin)
.current_dir(&env.project_dir)
.arg("build")
.output()
.expect("Failed to run sbpf build");

assert!(
!output.status.success(),
"Build should fail when include file is missing"
);

let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("nonexistent") || stderr.contains("Failed to read"),
"Error message should mention the missing file: {}",
stderr
);

env.cleanup();
}
11 changes: 11 additions & 0 deletions tests/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,14 @@ pub fn update_assembly_file(env: &TestEnv, project_name: &str, content: &str) {
.unwrap_or_else(|_| panic!("Failed to write new {}.s content", project_name));
println!("✅ Updated {}.s with specified content", project_name);
}

/// Write an include file alongside the main assembly file
#[allow(dead_code)]
pub fn write_include_file(env: &TestEnv, project_name: &str, filename: &str, content: &str) {
let include_path = env
.project_dir
.join(format!("src/{}/{}", project_name, filename));
fs::write(&include_path, content)
.unwrap_or_else(|_| panic!("Failed to write include file {}", filename));
println!("✅ Wrote include file {}", filename);
}