use crate::publish::package::{BuildProfile, PREFERRING_RELEASE_ORDER, find_built_artifact};
use crate::publish::platform::RustTarget;
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeLibraryStageStatus {
Staged,
Missing,
}
fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<()> {
fs::create_dir_all(dst).context(format!("creating directory: {}", dst.display()))?;
for entry in fs::read_dir(src).context(format!("reading directory: {}", src.display()))? {
let entry = entry.context(format!("reading entry in {}", src.display()))?;
let path = entry.path();
let file_name = entry.file_name();
let dst_path = dst.join(&file_name);
if path.is_dir() {
copy_dir_recursive(&path, &dst_path)?;
} else {
fs::copy(&path, &dst_path)
.with_context(|| format!("copying file {} to {}", path.display(), dst_path.display()))?;
}
}
Ok(())
}
#[derive(Debug, Clone)]
struct NativeLibPattern {
rid: &'static str,
rust_target: &'static str,
formats: &'static [NativeLibFormat],
}
#[derive(Debug, Clone, Copy)]
enum NativeLibFormat {
MacosDylib,
MacosFramework,
UnixSharedObject,
WindowsDll,
}
impl NativeLibFormat {
fn filename(self, stem: &str) -> String {
match self {
Self::MacosDylib => format!("lib{stem}.dylib"),
Self::MacosFramework => format!("{stem}.framework/{stem}"),
Self::UnixSharedObject => format!("lib{stem}.so"),
Self::WindowsDll => format!("{stem}.dll"),
}
}
}
const MACOS_NATIVE_LIB_FORMATS: &[NativeLibFormat] = &[NativeLibFormat::MacosDylib, NativeLibFormat::MacosFramework];
const LINUX_NATIVE_LIB_FORMATS: &[NativeLibFormat] = &[NativeLibFormat::UnixSharedObject];
const WINDOWS_NATIVE_LIB_FORMATS: &[NativeLibFormat] = &[NativeLibFormat::WindowsDll];
const NATIVE_LIB_PATTERNS: &[NativeLibPattern] = &[
NativeLibPattern {
rid: "macos-x64",
rust_target: "x86_64-apple-darwin",
formats: MACOS_NATIVE_LIB_FORMATS,
},
NativeLibPattern {
rid: "macos-arm64",
rust_target: "aarch64-apple-darwin",
formats: MACOS_NATIVE_LIB_FORMATS,
},
NativeLibPattern {
rid: "linux-x64",
rust_target: "x86_64-unknown-linux-gnu",
formats: LINUX_NATIVE_LIB_FORMATS,
},
NativeLibPattern {
rid: "linux-arm64",
rust_target: "aarch64-unknown-linux-gnu",
formats: LINUX_NATIVE_LIB_FORMATS,
},
NativeLibPattern {
rid: "windows-x64",
rust_target: "x86_64-pc-windows-msvc",
formats: WINDOWS_NATIVE_LIB_FORMATS,
},
NativeLibPattern {
rid: "windows-arm64",
rust_target: "aarch64-pc-windows-msvc",
formats: WINDOWS_NATIVE_LIB_FORMATS,
},
];
fn find_native_libraries(
workspace_root: &Path,
rust_target: &str,
filenames: &[String],
profiles: &[BuildProfile],
) -> Result<Vec<(PathBuf, PathBuf)>> {
let target = RustTarget::parse(rust_target)
.with_context(|| format!("parsing built-in Dart native-library target triple '{rust_target}'"))?;
let mut found = Vec::new();
for filename in filenames {
for &profile in profiles {
if let Ok(lib_path) = find_built_artifact(workspace_root, &target, filename, profile) {
found.push((lib_path, PathBuf::from(filename)));
break;
}
}
}
Ok(found)
}
pub fn stage_dart_native_libraries(
workspace_root: &Path,
package_root: &Path,
stem: &str,
profile: BuildProfile,
) -> Result<NativeLibraryStageStatus> {
stage_dart_native_libraries_for_profiles(workspace_root, package_root, stem, std::slice::from_ref(&profile))
}
pub fn stage_dart_native_libraries_preferring_release(
workspace_root: &Path,
package_root: &Path,
stem: &str,
) -> Result<NativeLibraryStageStatus> {
stage_dart_native_libraries_for_profiles(workspace_root, package_root, stem, &PREFERRING_RELEASE_ORDER)
}
fn stage_dart_native_libraries_for_profiles(
workspace_root: &Path,
package_root: &Path,
stem: &str,
profiles: &[BuildProfile],
) -> Result<NativeLibraryStageStatus> {
let native_base = package_root.join("lib/src/native");
let mut staged_any = false;
for pattern in NATIVE_LIB_PATTERNS {
let filenames = pattern
.formats
.iter()
.map(|format| format.filename(stem))
.collect::<Vec<_>>();
let libs = find_native_libraries(workspace_root, pattern.rust_target, &filenames, profiles)?;
if libs.is_empty() {
continue;
}
let rid_dir = native_base.join(pattern.rid);
fs::create_dir_all(&rid_dir).context(format!("creating native library directory: {}", rid_dir.display()))?;
for (lib_path, relative_path) in libs {
let dest = rid_dir.join(relative_path);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)
.with_context(|| format!("creating native library parent directory: {}", parent.display()))?;
}
if lib_path.is_dir() {
copy_dir_recursive(&lib_path, &dest).with_context(|| {
format!(
"copying native library directory {} to {}",
lib_path.display(),
dest.display()
)
})?;
} else {
fs::copy(&lib_path, &dest)
.with_context(|| format!("copying native library {} to {}", lib_path.display(), dest.display()))?;
}
staged_any = true;
}
}
Ok(if staged_any {
NativeLibraryStageStatus::Staged
} else {
NativeLibraryStageStatus::Missing
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stage_native_libraries_uses_package_stem() {
let tmp = tempfile::tempdir().expect("tempdir");
let target_dir = tmp.path().join("target/aarch64-apple-darwin/release");
fs::create_dir_all(&target_dir).unwrap();
fs::write(target_dir.join("libmy_lib_dart.dylib"), "native").unwrap();
let package_root = tmp.path().join("packages/dart");
fs::create_dir_all(&package_root).unwrap();
let status =
stage_dart_native_libraries(tmp.path(), &package_root, "my_lib_dart", BuildProfile::Release).unwrap();
assert_eq!(status, NativeLibraryStageStatus::Staged);
assert!(
package_root
.join("lib/src/native/macos-arm64/libmy_lib_dart.dylib")
.exists()
);
}
#[test]
fn stage_native_libraries_preserves_framework_layout() {
let tmp = tempfile::tempdir().expect("tempdir");
let target_dir = tmp
.path()
.join("target/aarch64-apple-darwin/release/my_lib_dart.framework");
fs::create_dir_all(&target_dir).unwrap();
fs::write(target_dir.join("my_lib_dart"), "native").unwrap();
let package_root = tmp.path().join("packages/dart");
fs::create_dir_all(&package_root).unwrap();
let status =
stage_dart_native_libraries(tmp.path(), &package_root, "my_lib_dart", BuildProfile::Release).unwrap();
assert_eq!(status, NativeLibraryStageStatus::Staged);
assert!(
package_root
.join("lib/src/native/macos-arm64/my_lib_dart.framework/my_lib_dart")
.exists()
);
}
#[test]
fn stage_native_libraries_debug_profile_finds_a_debug_only_build() {
let tmp = tempfile::tempdir().expect("tempdir");
let target_dir = tmp.path().join("target/aarch64-apple-darwin/debug");
fs::create_dir_all(&target_dir).unwrap();
fs::write(target_dir.join("libmy_lib_dart.dylib"), "debug-native").unwrap();
let package_root = tmp.path().join("packages/dart");
fs::create_dir_all(&package_root).unwrap();
let status =
stage_dart_native_libraries(tmp.path(), &package_root, "my_lib_dart", BuildProfile::Debug).unwrap();
assert_eq!(status, NativeLibraryStageStatus::Staged);
let staged = fs::read(package_root.join("lib/src/native/macos-arm64/libmy_lib_dart.dylib")).unwrap();
assert_eq!(
staged, b"debug-native",
"must stage the debug-profile bytes, not substitute anything else"
);
}
#[test]
fn debug_only_build_is_missing_for_a_release_request() {
let tmp = tempfile::tempdir().expect("tempdir");
let target_dir = tmp.path().join("target/aarch64-apple-darwin/debug");
fs::create_dir_all(&target_dir).unwrap();
fs::write(target_dir.join("libmy_lib_dart.dylib"), "debug-native").unwrap();
let package_root = tmp.path().join("packages/dart");
fs::create_dir_all(&package_root).unwrap();
let status =
stage_dart_native_libraries(tmp.path(), &package_root, "my_lib_dart", BuildProfile::Release).unwrap();
assert_eq!(
status,
NativeLibraryStageStatus::Missing,
"a debug-only build must not satisfy a release staging request"
);
assert!(
!package_root.join("lib/src/native").exists(),
"no destination directory should be created when nothing matched the requested profile"
);
}
#[test]
fn explicit_debug_request_never_prefers_a_stale_release_artifact() {
let tmp = tempfile::tempdir().expect("tempdir");
let release_dir = tmp.path().join("target/aarch64-apple-darwin/release");
fs::create_dir_all(&release_dir).unwrap();
fs::write(
release_dir.join("libmy_lib_dart.dylib"),
"STALE-RELEASE-FROM-EARLIER-RUN",
)
.unwrap();
let debug_dir = tmp.path().join("target/aarch64-apple-darwin/debug");
fs::create_dir_all(&debug_dir).unwrap();
fs::write(debug_dir.join("libmy_lib_dart.dylib"), "fresh-debug-bytes").unwrap();
let package_root = tmp.path().join("packages/dart");
fs::create_dir_all(&package_root).unwrap();
stage_dart_native_libraries(tmp.path(), &package_root, "my_lib_dart", BuildProfile::Debug).unwrap();
let staged = fs::read(package_root.join("lib/src/native/macos-arm64/libmy_lib_dart.dylib")).unwrap();
assert_eq!(
staged, b"fresh-debug-bytes",
"expected the fresh debug bytes, got the stale release copy instead"
);
}
#[test]
fn preferring_release_prefers_release_when_both_profiles_exist() {
let tmp = tempfile::tempdir().expect("tempdir");
let release_dir = tmp.path().join("target/aarch64-apple-darwin/release");
fs::create_dir_all(&release_dir).unwrap();
fs::write(release_dir.join("libmy_lib_dart.dylib"), "release-bytes").unwrap();
let debug_dir = tmp.path().join("target/aarch64-apple-darwin/debug");
fs::create_dir_all(&debug_dir).unwrap();
fs::write(debug_dir.join("libmy_lib_dart.dylib"), "debug-bytes").unwrap();
let package_root = tmp.path().join("packages/dart");
fs::create_dir_all(&package_root).unwrap();
stage_dart_native_libraries_preferring_release(tmp.path(), &package_root, "my_lib_dart").unwrap();
let staged = fs::read(package_root.join("lib/src/native/macos-arm64/libmy_lib_dart.dylib")).unwrap();
assert_eq!(
staged, b"release-bytes",
"release must win when both profiles are present"
);
}
#[test]
fn preferring_release_falls_back_to_debug_when_release_absent() {
let tmp = tempfile::tempdir().expect("tempdir");
let debug_dir = tmp.path().join("target/aarch64-apple-darwin/debug");
fs::create_dir_all(&debug_dir).unwrap();
fs::write(debug_dir.join("libmy_lib_dart.dylib"), "debug-bytes").unwrap();
let package_root = tmp.path().join("packages/dart");
fs::create_dir_all(&package_root).unwrap();
let status = stage_dart_native_libraries_preferring_release(tmp.path(), &package_root, "my_lib_dart").unwrap();
assert_eq!(status, NativeLibraryStageStatus::Staged);
let staged = fs::read(package_root.join("lib/src/native/macos-arm64/libmy_lib_dart.dylib")).unwrap();
assert_eq!(staged, b"debug-bytes");
}
#[test]
fn missing_native_libraries_are_a_development_no_op() {
let tmp = tempfile::tempdir().expect("tempdir");
let package_root = tmp.path().join("packages/dart");
fs::create_dir_all(&package_root).unwrap();
let status =
stage_dart_native_libraries(tmp.path(), &package_root, "my_lib_dart", BuildProfile::Release).unwrap();
assert_eq!(status, NativeLibraryStageStatus::Missing);
assert!(!package_root.join("lib/src/native").exists());
}
}