use crate::snippets::error::Result;
use crate::snippets::validators::native_library::{directory_has_ffi_library, linkable_library_names};
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 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(declared) = declared_ffi_library(manifest)? else {
return Ok(None);
};
match locate_ffi_library(&declared) {
Some(directory) if directory == declared.release_directory => Ok(None),
Some(directory) => Ok(Some((declared.option_name, directory))),
None => Ok(None),
}
}
pub(crate) fn unresolvable_ffi_library(manifest: &Path) -> Result<Option<PathBuf>> {
let Some(declared) = declared_ffi_library(manifest)? else {
return Ok(None);
};
if locate_ffi_library(&declared).is_some() {
return Ok(None);
}
let name = linkable_library_names(&declared.library_name)
.into_iter()
.next()
.expect("every host links at least one library name");
Ok(Some(declared.release_directory.join(name)))
}
struct DeclaredFfiLibrary {
option_name: String,
release_directory: PathBuf,
library_name: String,
}
fn declared_ffi_library(manifest: &Path) -> Result<Option<DeclaredFfiLibrary>> {
let Some((option_name, release_directory)) = zig_manifest_library_path_option(manifest)? else {
return Ok(None);
};
let source = std::fs::read_to_string(manifest)?;
let Some(library_name) = zig_manifest_link_library_name(&source) else {
return Ok(None);
};
Ok(Some(DeclaredFfiLibrary {
option_name,
release_directory,
library_name,
}))
}
fn locate_ffi_library(declared: &DeclaredFfiLibrary) -> Option<PathBuf> {
if directory_has_ffi_library(&declared.release_directory, &declared.library_name) {
return Some(declared.release_directory.clone());
}
let debug = sibling_profile_directory(&declared.release_directory)?;
directory_has_ffi_library(&debug, &declared.library_name).then_some(debug)
}
#[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"
)
}
fn write_stand_in_library(directory: &Path, lib_name: &str) {
std::fs::create_dir_all(directory).unwrap();
let name = linkable_library_names(lib_name)
.into_iter()
.next()
.expect("every host links at least one library name");
std::fs::write(directory.join(name), "fake").unwrap();
}
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");
write_stand_in_library(&directory.path().join("debug"), "sample_ffi");
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");
write_stand_in_library(&directory.path().join("release"), "sample_ffi");
write_stand_in_library(&directory.path().join("debug"), "sample_ffi");
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 an_unbuilt_ffi_library_is_reported_with_the_path_a_build_would_fill() {
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();
let missing = unresolvable_ffi_library(&manifest).unwrap().expect("nothing is built");
assert!(
missing.starts_with(package.join("../release")),
"the report must name the release directory the manifest searches: {}",
missing.display()
);
assert!(
missing.to_string_lossy().contains("sample_ffi"),
"the report must name the library: {}",
missing.display()
);
}
#[test]
fn a_built_ffi_library_leaves_nothing_for_the_preflight_to_report() {
let directory = tempfile::tempdir().expect("project directory");
write_stand_in_library(&directory.path().join("release"), "sample_ffi");
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!(unresolvable_ffi_library(&manifest).unwrap(), None);
}
#[test]
fn a_debug_only_ffi_library_leaves_nothing_for_the_preflight_to_report() {
let directory = tempfile::tempdir().expect("project directory");
write_stand_in_library(&directory.path().join("debug"), "sample_ffi");
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!(unresolvable_ffi_library(&manifest).unwrap(), None);
}
#[test]
fn directory_has_ffi_library_never_credits_a_deps_only_copy() {
let directory = tempfile::tempdir().expect("project directory");
write_stand_in_library(&directory.path().join("deps"), "sample_ffi");
assert!(!directory_has_ffi_library(directory.path(), "sample_ffi"));
}
#[test]
fn directory_has_ffi_library_finds_every_name_this_host_can_link() {
for name in linkable_library_names("sample_ffi") {
let directory = tempfile::tempdir().expect("project directory");
std::fs::write(directory.path().join(&name), "fake").unwrap();
assert!(
directory_has_ffi_library(directory.path(), "sample_ffi"),
"{name} is a name this host links but the probe does not find"
);
}
}
#[test]
fn the_probe_names_match_what_this_host_actually_produces() {
let expected: [&str; 3] = if cfg!(windows) {
["sample_ffi.dll", "sample_ffi.lib", "libsample_ffi.a"]
} else {
["libsample_ffi.dylib", "libsample_ffi.so", "libsample_ffi.a"]
};
assert_eq!(linkable_library_names("sample_ffi"), expected);
let directory = tempfile::tempdir().expect("project directory");
let never_produced = if cfg!(windows) {
"libsample_ffi.dll"
} else {
"sample_ffi.so"
};
std::fs::write(directory.path().join(never_produced), "fake").unwrap();
assert!(
!directory_has_ffi_library(directory.path(), "sample_ffi"),
"{never_produced} is not a name this host produces or links, so it must not count"
);
}
}