use crate::snippets::error::Result;
use std::path::{Path, PathBuf};
pub(crate) fn zig_manifest_include_paths(manifest: &Path) -> Result<Vec<String>> {
const DECLARATION: &str = "addIncludePath(.{ .cwd_relative = ";
let source = std::fs::read_to_string(manifest)?;
let mut paths: Vec<String> = Vec::new();
for occurrence in source.split(DECLARATION).skip(1) {
let Some(end) = occurrence.find(" })") else {
continue;
};
let expression = occurrence[..end].trim();
let Some(path) = string_literal(expression)
.map(str::to_owned)
.or_else(|| binding_default(&source, expression))
else {
continue;
};
if !paths.contains(&path) {
paths.push(path);
}
}
Ok(paths)
}
fn string_literal(expression: &str) -> Option<&str> {
expression.strip_prefix('"')?.strip_suffix('"')
}
const MAX_BINDING_INDIRECTIONS: usize = 4;
fn resolve_binding_statement(source: &str, name: &str) -> Option<String> {
const REBASE: &str = "b.pathResolve(&.{";
let mut name = name.to_owned();
for _ in 0..MAX_BINDING_INDIRECTIONS {
let marker = format!("const {name} = ");
let start = source.find(&marker)? + marker.len();
let statement = source[start..]
.split_once(';')
.map_or(&source[start..], |(head, _)| head);
let Some(arguments) = statement.strip_prefix(REBASE) else {
return Some(statement.to_owned());
};
let end = arguments.find("})")?;
name = arguments[..end].rsplit(',').next()?.trim().to_owned();
}
None
}
fn binding_default(source: &str, name: &str) -> Option<String> {
orelse_literal(&resolve_binding_statement(source, name)?)
}
fn orelse_literal(statement: &str) -> Option<String> {
const ORELSE: &str = "orelse ";
let default = statement.find(ORELSE)? + ORELSE.len();
let literal = statement[default..].trim_start().strip_prefix('"')?;
let end = literal.find('"')?;
Some(literal[..end].to_owned())
}
fn option_name_in_statement(statement: &str) -> Option<String> {
let orelse_at = statement.find("orelse ")?;
let head = &statement[..orelse_at];
let start = head.find('"')? + 1;
let end = start + head[start..].find('"')?;
Some(head[start..end].to_owned())
}
pub(crate) fn zig_manifest_library_path_option(manifest: &Path) -> Result<Option<(String, PathBuf)>> {
const DECLARATION: &str = "addLibraryPath(.{ .cwd_relative = ";
let source = std::fs::read_to_string(manifest)?;
let Some(occurrence) = source.split(DECLARATION).nth(1) else {
return Ok(None);
};
let Some(end) = occurrence.find(" })") else {
return Ok(None);
};
let expression = occurrence[..end].trim();
let Some(statement) = resolve_binding_statement(&source, expression) else {
return Ok(None);
};
let (Some(option_name), Some(default_literal)) = (option_name_in_statement(&statement), orelse_literal(&statement))
else {
return Ok(None);
};
let build_root = manifest.parent().unwrap_or(Path::new("."));
Ok(Some((option_name, build_root.join(default_literal))))
}
fn zig_manifest_link_library_name(source: &str) -> Option<String> {
const DECLARATION: &str = "linkSystemLibrary(\"";
let start = source.find(DECLARATION)? + DECLARATION.len();
let end = start + source[start..].find('"')?;
Some(source[start..end].to_owned())
}
fn directory_has_ffi_library(directory: &Path, lib_name: &str) -> bool {
const EXTENSIONS: [&str; 4] = ["dylib", "so", "a", "dll"];
EXTENSIONS
.iter()
.any(|extension| directory.join(format!("lib{lib_name}.{extension}")).is_file())
}
fn sibling_profile_directory(release_dir: &Path) -> Option<PathBuf> {
(release_dir.file_name()?.to_str()? == "release").then(|| release_dir.with_file_name("debug"))
}
pub(crate) fn resolve_ffi_library_override(manifest: &Path) -> Result<Option<(String, PathBuf)>> {
let Some((option_name, release_dir)) = zig_manifest_library_path_option(manifest)? else {
return Ok(None);
};
let source = std::fs::read_to_string(manifest)?;
let Some(lib_name) = zig_manifest_link_library_name(&source) else {
return Ok(None);
};
if directory_has_ffi_library(&release_dir, &lib_name) {
return Ok(None);
}
let Some(debug_dir) = sibling_profile_directory(&release_dir) else {
return Ok(None);
};
Ok(directory_has_ffi_library(&debug_dir, &lib_name).then_some((option_name, debug_dir)))
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
pub(crate) fn sample_build_zig(with_include: bool) -> String {
let include = if with_include {
"module.addIncludePath(.{ .cwd_relative = ffi_include });\n"
} else {
""
};
format!(
"const std = @import(\"std\");\n\
pub fn build(b: *std.Build) void {{\n\
\x20 const ffi_include = b.option(\n\
\x20 []const u8,\n\
\x20 \"ffi_include_path\",\n\
\x20 \"Path to directory containing the FFI C header\"\n\
\x20 ) orelse \"vendor/include\";\n\
\x20 const module = b.addModule(\"sample_binding\", .{{\n\
\x20 .root_source_file = b.path(\"src/root.zig\"),\n\
\x20 .link_libc = true,\n\
\x20 }});\n\
\x20 {include}\
}}\n"
)
}
pub(crate) fn build_root_rebased_build_zig(package_name: &str) -> String {
format!(
"const std = @import(\"std\");\n\
pub fn build(b: *std.Build) void {{\n\
\x20 const target = b.standardTargetOptions(.{{}});\n\
\x20 const optimize = b.standardOptimizeOption(.{{}});\n\
\x20 const build_root = b.build_root.path orelse \".\";\n\
\x20 const ffi_include_option = b.option(\n\
\x20 []const u8,\n\
\x20 \"ffi_include_path\",\n\
\x20 \"Path to directory containing the FFI C header\"\n\
\x20 ) orelse \"vendor/include\";\n\
\x20 const ffi_include = b.pathResolve(&.{{ build_root, ffi_include_option }});\n\
\x20 const module = b.addModule(\"{package_name}\", .{{\n\
\x20 .root_source_file = b.path(\"src/root.zig\"),\n\
\x20 .target = target,\n\
\x20 .optimize = optimize,\n\
\x20 .link_libc = true,\n\
\x20 }});\n\
\x20 module.addIncludePath(.{{ .cwd_relative = ffi_include }});\n\
}}\n"
)
}
pub(crate) fn build_root_rebased_ffi_path_build_zig(lib_name: &str, default_dir: &str) -> String {
format!(
"const std = @import(\"std\");\n\
pub fn build(b: *std.Build) void {{\n\
\x20 const target = b.standardTargetOptions(.{{}});\n\
\x20 const optimize = b.standardOptimizeOption(.{{}});\n\
\x20 const build_root = b.build_root.path orelse \".\";\n\
\x20 const ffi_path_option = b.option(\n\
\x20 []const u8,\n\
\x20 \"ffi_path\",\n\
\x20 \"Path to directory containing lib{lib_name}.{{dylib,so,dll,a}}\"\n\
\x20 ) orelse \"{default_dir}\";\n\
\x20 const ffi_path = b.pathResolve(&.{{ build_root, ffi_path_option }});\n\
\x20 const module = b.addModule(\"sample_binding\", .{{\n\
\x20 .root_source_file = b.path(\"src/root.zig\"),\n\
\x20 .target = target,\n\
\x20 .optimize = optimize,\n\
\x20 .link_libc = true,\n\
\x20 }});\n\
\x20 module.addLibraryPath(.{{ .cwd_relative = ffi_path }});\n\
\x20 module.linkSystemLibrary(\"{lib_name}\", .{{}});\n\
}}\n"
)
}
#[test]
fn manifest_include_paths_resolve_through_the_build_option_default() {
let directory = tempfile::tempdir().unwrap();
let manifest = directory.path().join("build.zig");
std::fs::write(&manifest, sample_build_zig(true)).unwrap();
let paths = zig_manifest_include_paths(&manifest).unwrap();
assert_eq!(paths, ["vendor/include"]);
}
#[test]
fn manifest_include_paths_accept_a_direct_string_literal() {
let directory = tempfile::tempdir().unwrap();
let manifest = directory.path().join("build.zig");
std::fs::write(
&manifest,
"const module = b.addModule(\"sample_binding\", .{\n .root_source_file = b.path(\"src/root.zig\"),\n});\nmodule.addIncludePath(.{ .cwd_relative = \"include\" });\n",
)
.unwrap();
let paths = zig_manifest_include_paths(&manifest).unwrap();
assert_eq!(paths, ["include"]);
}
#[test]
fn a_manifest_without_an_include_declaration_contributes_no_paths() {
let directory = tempfile::tempdir().unwrap();
let manifest = directory.path().join("build.zig");
std::fs::write(&manifest, sample_build_zig(false)).unwrap();
assert!(zig_manifest_include_paths(&manifest).unwrap().is_empty());
}
#[test]
fn manifest_include_paths_resolve_through_a_build_root_rebased_binding() {
let directory = tempfile::tempdir().expect("project directory");
let manifest = directory.path().join("build.zig");
std::fs::write(&manifest, build_root_rebased_build_zig("sample_binding")).unwrap();
let paths = zig_manifest_include_paths(&manifest).unwrap();
assert_eq!(paths, ["vendor/include"]);
}
#[test]
fn binding_default_gives_up_rather_than_looping_on_a_cyclic_binding() {
let source = "const a = b.pathResolve(&.{ root, b_name });\nconst b_name = b.pathResolve(&.{ root, a });\n";
assert_eq!(binding_default(source, "a"), None);
}
#[test]
fn resolve_ffi_library_override_falls_back_to_debug_when_release_is_missing() {
let directory = tempfile::tempdir().expect("project directory");
std::fs::create_dir_all(directory.path().join("debug")).unwrap();
std::fs::write(directory.path().join("debug/libsample_ffi.dylib"), "fake").unwrap();
let package = directory.path().join("package");
std::fs::create_dir(&package).unwrap();
let manifest = package.join("build.zig");
std::fs::write(
&manifest,
build_root_rebased_ffi_path_build_zig("sample_ffi", "../release"),
)
.unwrap();
let (option_name, resolved) = resolve_ffi_library_override(&manifest).unwrap().unwrap();
assert_eq!(option_name, "ffi_path");
assert_eq!(resolved, package.join("../debug"));
}
#[test]
fn resolve_ffi_library_override_is_a_no_op_when_release_already_has_the_library() {
let directory = tempfile::tempdir().expect("project directory");
std::fs::create_dir_all(directory.path().join("release")).unwrap();
std::fs::write(directory.path().join("release/libsample_ffi.dylib"), "fake").unwrap();
std::fs::create_dir_all(directory.path().join("debug")).unwrap();
std::fs::write(directory.path().join("debug/libsample_ffi.dylib"), "fake").unwrap();
let package = directory.path().join("package");
std::fs::create_dir(&package).unwrap();
let manifest = package.join("build.zig");
std::fs::write(
&manifest,
build_root_rebased_ffi_path_build_zig("sample_ffi", "../release"),
)
.unwrap();
assert_eq!(resolve_ffi_library_override(&manifest).unwrap(), None);
}
#[test]
fn resolve_ffi_library_override_is_a_no_op_when_neither_profile_is_built() {
let directory = tempfile::tempdir().expect("project directory");
let package = directory.path().join("package");
std::fs::create_dir(&package).unwrap();
let manifest = package.join("build.zig");
std::fs::write(
&manifest,
build_root_rebased_ffi_path_build_zig("sample_ffi", "../release"),
)
.unwrap();
assert_eq!(resolve_ffi_library_override(&manifest).unwrap(), None);
}
#[test]
fn directory_has_ffi_library_never_credits_a_deps_only_copy() {
let directory = tempfile::tempdir().expect("project directory");
std::fs::create_dir_all(directory.path().join("deps")).unwrap();
std::fs::write(directory.path().join("deps/libsample_ffi.dylib"), "fake").unwrap();
assert!(!directory_has_ffi_library(directory.path(), "sample_ffi"));
}
}