use super::PackageArtifact;
use crate::core::config::ResolvedCrateConfig;
use crate::core::config::extras::Language;
use crate::publish::platform::RustTarget;
use crate::scaffold::naming::csharp_package_id;
use crate::scaffold::{
PUBLISHED_RUNTIME_IDENTIFIERS, render_csharp_csproj, render_csharp_runtime_csproj,
render_csharp_runtime_json_template,
};
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};
pub fn package_csharp(
config: &ResolvedCrateConfig,
target: &RustTarget,
workspace_root: &Path,
output_dir: &Path,
version: &str,
) -> Result<Vec<PackageArtifact>> {
let lib_name = config.ffi_lib_name();
let shared_lib = target.shared_lib_name(&lib_name);
let rid = csharp_rid(config, target);
let namespace = config.csharp_namespace();
let package_id = csharp_package_id(config);
let lib_src = crate::publish::package::find_built_artifact(workspace_root, target, &shared_lib)?;
let pkg_dir_str = config.package_dir(Language::Csharp);
let pkg_dir = workspace_root.join(&pkg_dir_str);
let meta_dir = pkg_dir.join(&namespace);
stage_native(&meta_dir, &rid, &lib_src, &shared_lib)?;
let csproj = find_csproj(&pkg_dir, &namespace)?;
fs::write(&csproj, render_csharp_csproj(config, version))
.with_context(|| format!("regenerating csproj at {}", csproj.display()))?;
tracing::debug!(path = %csproj.display(), "regenerated csproj from scaffold template");
let runtime_json = meta_dir.join("runtime.json");
let runtime_json_content = render_csharp_runtime_json_template(config).replace("{{VERSION}}", version);
fs::write(&runtime_json, runtime_json_content)
.with_context(|| format!("rendering runtime.json at {}", runtime_json.display()))?;
tracing::debug!(path = %runtime_json.display(), "rendered runtime.json from template");
let runtime_csproj = runtime_csproj_path(&pkg_dir, &namespace);
fs::create_dir_all(runtime_csproj.parent().context("runtime csproj has no parent")?)
.with_context(|| format!("creating {}", runtime_csproj.display()))?;
fs::write(&runtime_csproj, render_csharp_runtime_csproj(config, version))
.with_context(|| format!("regenerating runtime csproj at {}", runtime_csproj.display()))?;
tracing::debug!(path = %runtime_csproj.display(), "regenerated runtime csproj from scaffold template");
let abs_output_dir = output_dir.canonicalize().unwrap_or_else(|_| output_dir.to_path_buf());
let mut artifacts = Vec::new();
let meta_proj_dir = csproj.parent().context("csproj has no parent")?;
let meta_proj_name = file_name(&csproj)?;
let pack_meta_cmd = format!(
"dotnet pack {proj} --configuration Release -p:Version={version} --output {out}",
proj = meta_proj_name,
out = abs_output_dir.display()
);
crate::publish::run_shell_command_in(&pack_meta_cmd, meta_proj_dir)?;
let meta_nupkg = find_nupkg(&abs_output_dir, &package_id, version)?;
artifacts.push(PackageArtifact {
name: file_name(&meta_nupkg)?,
path: meta_nupkg,
checksum: None,
});
let runtime_proj_dir = runtime_csproj.parent().context("runtime csproj has no parent")?;
let runtime_proj_name = file_name(&runtime_csproj)?;
for (rid_name, _triple) in PUBLISHED_RUNTIME_IDENTIFIERS
.iter()
.filter(|(_, t)| config.target_enabled(t))
{
let native_dir = meta_dir.join("runtimes").join(rid_name).join("native");
if !has_staged_native(&native_dir) {
continue;
}
let pack_runtime_cmd = format!(
"dotnet pack {proj} --configuration Release -p:Version={version} -p:PublishedRID={rid_name} --output {out}",
proj = runtime_proj_name,
out = abs_output_dir.display()
);
crate::publish::run_shell_command_in(&pack_runtime_cmd, runtime_proj_dir)?;
let runtime_package_id = format!("{package_id}.runtime.{rid_name}");
let runtime_nupkg = find_nupkg(&abs_output_dir, &runtime_package_id, version)?;
artifacts.push(PackageArtifact {
name: file_name(&runtime_nupkg)?,
path: runtime_nupkg,
checksum: None,
});
}
Ok(artifacts)
}
fn stage_native(meta_dir: &Path, rid: &str, lib_src: &Path, shared_lib: &str) -> Result<()> {
let runtimes_dir = meta_dir.join("runtimes").join(rid).join("native");
fs::create_dir_all(&runtimes_dir).with_context(|| format!("creating runtimes dir {}", runtimes_dir.display()))?;
let staged = runtimes_dir.join(shared_lib);
fs::copy(lib_src, &staged).with_context(|| format!("staging {} to {}", lib_src.display(), staged.display()))?;
Ok(())
}
fn has_staged_native(native_dir: &Path) -> bool {
fs::read_dir(native_dir)
.map(|mut entries| entries.next().is_some())
.unwrap_or(false)
}
fn file_name(path: &Path) -> Result<String> {
Ok(path
.file_name()
.with_context(|| format!("{} has no file name", path.display()))?
.to_string_lossy()
.to_string())
}
fn csharp_rid(config: &ResolvedCrateConfig, target: &RustTarget) -> String {
if let Some(publish) = &config.publish {
if let Some(lang_cfg) = publish.languages.get("csharp") {
if let Some(override_rid) = &lang_cfg.csharp_rid {
return override_rid.clone();
}
}
}
target.platform_for(Language::Csharp)
}
fn find_csproj(pkg_dir: &Path, namespace: &str) -> Result<PathBuf> {
let candidate = pkg_dir.join(namespace).join(format!("{namespace}.csproj"));
if candidate.exists() {
return Ok(candidate);
}
if pkg_dir.exists() {
for entry in fs::read_dir(pkg_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
for inner in fs::read_dir(&path)? {
let inner = inner?;
let ip = inner.path();
if ip.extension().is_some_and(|e| e == "csproj") {
return Ok(ip);
}
}
}
if path.extension().is_some_and(|e| e == "csproj") {
return Ok(path);
}
}
}
anyhow::bail!("No .csproj found under {}", pkg_dir.display())
}
fn runtime_csproj_path(pkg_dir: &Path, namespace: &str) -> PathBuf {
pkg_dir
.join(format!("{namespace}.Runtime"))
.join(format!("{namespace}.Runtime.csproj"))
}
fn find_nupkg(output_dir: &Path, package_id: &str, version: &str) -> Result<PathBuf> {
let expected = output_dir.join(format!("{package_id}.{version}.nupkg"));
if expected.exists() {
return Ok(expected);
}
let candidates: Vec<PathBuf> = fs::read_dir(output_dir)?
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|e| e == "nupkg"))
.collect();
if candidates.len() == 1 {
return Ok(candidates.into_iter().next().expect("len checked"));
}
anyhow::bail!(
"no unambiguous .nupkg for {package_id}-{version} found in {} ({} candidate(s))",
output_dir.display(),
candidates.len()
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::NewAlefConfig;
fn minimal_config() -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["csharp"]
[[crates]]
name = "mylib"
sources = ["src/lib.rs"]
[crates.csharp]
namespace = "MyLib"
"#,
)
.unwrap();
cfg.resolve().unwrap().remove(0)
}
#[test]
fn rid_linux_x64() {
let config = minimal_config();
let t = RustTarget::parse("x86_64-unknown-linux-gnu").unwrap();
assert_eq!(csharp_rid(&config, &t), "linux-x64");
}
#[test]
fn rid_osx_arm64() {
let config = minimal_config();
let t = RustTarget::parse("aarch64-apple-darwin").unwrap();
assert_eq!(csharp_rid(&config, &t), "osx-arm64");
}
#[test]
fn rid_win_x64() {
let config = minimal_config();
let t = RustTarget::parse("x86_64-pc-windows-msvc").unwrap();
assert_eq!(csharp_rid(&config, &t), "win-x64");
}
#[test]
fn rid_linux_musl_x64() {
let config = minimal_config();
let t = RustTarget::parse("x86_64-unknown-linux-musl").unwrap();
assert_eq!(csharp_rid(&config, &t), "linux-musl-x64");
}
#[test]
fn rid_config_override() {
let cfg: NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["csharp"]
[[crates]]
name = "mylib"
sources = ["src/lib.rs"]
[crates.publish.languages.csharp]
csharp_rid = "linux-x64-custom"
"#,
)
.unwrap();
let config = cfg.resolve().unwrap().remove(0);
let t = RustTarget::parse("x86_64-unknown-linux-gnu").unwrap();
assert_eq!(csharp_rid(&config, &t), "linux-x64-custom");
}
#[test]
fn find_nupkg_expected() {
let tmp = tempfile::TempDir::new().unwrap();
let pkg = tmp.path().join("MyLib.1.0.0.nupkg");
std::fs::write(&pkg, b"fake").unwrap();
let result = find_nupkg(tmp.path(), "MyLib", "1.0.0").unwrap();
assert_eq!(result, pkg);
}
#[test]
fn find_nupkg_fallback_scan() {
let tmp = tempfile::TempDir::new().unwrap();
let pkg = tmp.path().join("SomeOtherName.1.0.0.nupkg");
std::fs::write(&pkg, b"fake").unwrap();
let result = find_nupkg(tmp.path(), "MyLib", "1.0.0").unwrap();
assert!(result.extension().unwrap() == "nupkg");
}
#[test]
fn find_nupkg_ambiguous_scan_errors() {
let tmp = tempfile::TempDir::new().unwrap();
std::fs::write(tmp.path().join("MyLib.runtime.linux-x64.1.0.0.nupkg"), b"fake").unwrap();
std::fs::write(tmp.path().join("MyLib.runtime.osx-arm64.1.0.0.nupkg"), b"fake").unwrap();
let result = find_nupkg(tmp.path(), "MyLib", "1.0.0");
assert!(result.is_err());
}
#[test]
fn find_nupkg_exact_match_ignores_sibling_runtime_packages() {
let tmp = tempfile::TempDir::new().unwrap();
let meta = tmp.path().join("MyLib.1.0.0.nupkg");
std::fs::write(&meta, b"fake").unwrap();
std::fs::write(tmp.path().join("MyLib.runtime.linux-x64.1.0.0.nupkg"), b"fake").unwrap();
let result = find_nupkg(tmp.path(), "MyLib", "1.0.0").unwrap();
assert_eq!(result, meta);
}
#[test]
fn runtime_csproj_path_matches_scaffold_layout() {
let pkg_dir = Path::new("/repo/packages/csharp");
let path = runtime_csproj_path(pkg_dir, "MyLib");
assert_eq!(
path,
Path::new("/repo/packages/csharp/MyLib.Runtime/MyLib.Runtime.csproj")
);
}
#[test]
fn has_staged_native_false_when_missing() {
let tmp = tempfile::TempDir::new().unwrap();
assert!(!has_staged_native(&tmp.path().join("runtimes/linux-x64/native")));
}
#[test]
fn has_staged_native_true_when_file_present() {
let tmp = tempfile::TempDir::new().unwrap();
let native_dir = tmp.path().join("runtimes/linux-x64/native");
std::fs::create_dir_all(&native_dir).unwrap();
std::fs::write(native_dir.join("libmylib.so"), b"fake").unwrap();
assert!(has_staged_native(&native_dir));
}
#[test]
fn stage_native_writes_expected_layout() {
let tmp = tempfile::TempDir::new().unwrap();
let lib_src = tmp.path().join("libmylib.so");
std::fs::write(&lib_src, b"fake-native").unwrap();
let meta_dir = tmp.path().join("meta");
stage_native(&meta_dir, "linux-x64", &lib_src, "libmylib.so").unwrap();
let staged = meta_dir.join("runtimes/linux-x64/native/libmylib.so");
assert!(staged.exists());
assert_eq!(std::fs::read(staged).unwrap(), b"fake-native");
}
#[test]
fn published_runtime_identifiers_filtered_by_target_enabled_and_staged() {
let config = minimal_config();
let enabled: Vec<&str> = PUBLISHED_RUNTIME_IDENTIFIERS
.iter()
.filter(|(_, t)| config.target_enabled(t))
.map(|(rid, _)| *rid)
.collect();
assert_eq!(enabled.len(), PUBLISHED_RUNTIME_IDENTIFIERS.len());
assert!(enabled.contains(&"linux-x64"));
assert!(enabled.contains(&"osx-arm64"));
assert!(enabled.contains(&"win-x64"));
}
}