Files
regorus/xtask/src/tasks/bindings/csharp.rs
Anand Krishnamoorthi e68e852ee3 feat(xtask): consolidate CI workflows onto xtask helpers (#542)
- split the xtask crate into structured modules for
  - bindings
  - ci
  - dev
  - util
  - no-std
- Adding commands for
  - ci-release/ci-debug
  - MUSL/no-std
  - per- binding language smoke tests
  - developer tasks (fmt, clippy, pre-commit, pre-push)
- refresh Cargo manifests/locks, binding readmes, and shared FFI helpers so every binding reuses the same preparation steps
- refactor GitHub Actions (release/debug, extensions, CodeQL, clippy, bindings) to call the new xtask commands
- Use rust-cache in ci workflows (microsoft qdk also does this)
- extend README with a contributor workflow section describing how xtask mirrors CI expectations
- update pre-commit and pre-push hooks to use the xtask dev commands

WORKAROUND:
When dotnet is run from an xtask, codeql tracer intercepts it an routes to a nonexistent binary.
Therefore in codeql workflow, xtask is not used for c# and instead dotnet is directly invoked.
Tracked by #545

closes #475

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-01-27 07:42:21 +05:30

485 lines
15 KiB
Rust

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::fs::{self, File};
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{anyhow, Context, Result};
use clap::Args;
use super::ffi;
use crate::tasks::util::{dotnet_host_arch, run_command, workspace_root};
use zip::read::ZipArchive;
/// Builds the Regorus C# NuGet package from local sources.
#[derive(Args, Clone)]
pub struct BuildNugetCommand {
/// Build the Rust FFI artefacts for the provided target triple (defaults to the host).
#[arg(long = "target", value_name = "TRIPLE")]
pub targets: Vec<String>,
/// Build the package in release mode (defaults to debug).
#[arg(long)]
pub release: bool,
/// Remove existing build artefacts before compiling.
#[arg(long)]
pub clean: bool,
/// Override the directory that contains compiled regorus FFI artefacts.
#[arg(long, value_name = "PATH")]
pub artifacts_dir: Option<PathBuf>,
/// Require all platform artefacts to exist before packing.
#[arg(long)]
pub enforce_artifacts: bool,
}
/// Parsed build options shared across tasks that need a NuGet package.
#[derive(Clone, Debug)]
pub struct BuildNugetConfig {
pub targets: Vec<String>,
pub release: bool,
pub clean: bool,
pub artifacts_dir: Option<PathBuf>,
pub enforce_artifacts: bool,
}
/// Result of a NuGet build, including generated artefacts.
#[derive(Debug)]
pub struct BuildNugetResult {
pub package_dir: PathBuf,
pub packages: Vec<PathBuf>,
}
/// Builds (or rebuilds) the C# NuGet package with the supplied configuration.
pub fn build_nuget_package(config: &BuildNugetConfig) -> Result<BuildNugetResult> {
let workspace_root = workspace_root();
let configuration = if config.release { "Release" } else { "Debug" };
let profile = ffi::profile_dir(config.release);
let base_artifacts_dir = if let Some(dir) = &config.artifacts_dir {
if !config.targets.is_empty() {
println!("Skipping FFI build for specified targets because --artifacts-dir is set.");
}
dir.clone()
} else {
let targets = ffi::resolve_targets(config.targets.clone())?;
ffi::build_targets(&workspace_root, &targets, config.release)?;
workspace_root.join("bindings/ffi/target")
};
let artifacts_dir = base_artifacts_dir.canonicalize().with_context(|| {
format!(
"failed to canonicalize FFI artefacts directory at {}",
base_artifacts_dir.display()
)
})?;
let package_dir = invoke_dotnet_pack(
&workspace_root,
&artifacts_dir,
configuration,
&profile,
!config.enforce_artifacts,
config.clean,
)?;
let packages = find_packages(&package_dir)?;
Ok(BuildNugetResult {
package_dir,
packages,
})
}
/// Returns all NuGet packages currently present in the given directory.
pub fn find_packages(package_dir: &Path) -> Result<Vec<PathBuf>> {
if !package_dir.exists() {
return Ok(Vec::new());
}
let mut packages = Vec::new();
let entries = fs::read_dir(package_dir).with_context(|| {
format!(
"failed to enumerate NuGet artefacts under {}",
package_dir.display()
)
})?;
for entry in entries {
let entry = entry?;
let path = entry.path();
if path
.extension()
.and_then(|ext| ext.to_str())
.map_or(false, |ext| ext.eq_ignore_ascii_case("nupkg"))
{
packages.push(path);
}
}
packages.sort();
Ok(packages)
}
fn invoke_dotnet_pack(
root: &Path,
artifacts_dir: &Path,
configuration: &str,
profile: &str,
ignore_missing: bool,
clean: bool,
) -> Result<PathBuf> {
let project_dir = root.join("bindings/csharp/Regorus");
let artifacts_dir_str = artifacts_dir
.to_str()
.ok_or_else(|| anyhow!("artefact directory path contains invalid UTF-8"))?;
let profile_arg = format!("/p:RegorusFFIArtifactsProfile={}", profile);
let dir_arg = format!("/p:RegorusFFIArtifactsDir={}", artifacts_dir_str);
if clean {
clean_msbuild_project(&project_dir, configuration)?;
let artefact_root = project_dir.join("bin").join(configuration);
if artefact_root.exists() {
fs::remove_dir_all(&artefact_root).with_context(|| {
format!(
"failed to remove existing NuGet artefacts from {}",
artefact_root.display()
)
})?;
}
}
let mut restore = Command::new("dotnet");
restore.current_dir(&project_dir);
restore.arg("restore");
run_command(restore, "dotnet restore")?;
let mut build = Command::new("dotnet");
build.current_dir(&project_dir);
build.arg("build");
build.arg("--no-restore");
build.arg("-c");
build.arg(configuration);
build.arg("--verbosity");
build.arg("minimal");
build.arg(&dir_arg);
build.arg(&profile_arg);
if ignore_missing {
build.arg("/p:IgnoreMissingArtifacts=true");
}
run_command(build, "dotnet build")?;
let mut pack = Command::new("dotnet");
pack.current_dir(&project_dir);
pack.arg("pack");
pack.arg("--no-build");
pack.arg("-c");
pack.arg(configuration);
pack.arg(&dir_arg);
pack.arg(&profile_arg);
if ignore_missing {
pack.arg("/p:IgnoreMissingArtifacts=true");
}
run_command(pack, "dotnet pack")?;
Ok(project_dir.join("bin").join(configuration))
}
impl BuildNugetCommand {
/// Entry point executed by the xtask harness.
pub fn run(&self) -> Result<()> {
let config = self.to_config();
let result = build_nuget_package(&config)?;
println!(
"NuGet package(s) available under {}",
result.package_dir.display()
);
if result.packages.is_empty() {
println!("No NuGet packages were produced; check earlier log output.");
} else {
print_package_listing(&result.packages)?;
}
Ok(())
}
pub fn to_config(&self) -> BuildNugetConfig {
BuildNugetConfig {
targets: self.targets.clone(),
release: self.release,
clean: self.clean,
artifacts_dir: self.artifacts_dir.clone(),
enforce_artifacts: self.enforce_artifacts,
}
}
}
fn print_package_listing(packages: &[PathBuf]) -> Result<()> {
for package in packages {
println!("Contents of {}:", package.display());
let file =
File::open(package).with_context(|| format!("failed to open {}", package.display()))?;
let mut archive = ZipArchive::new(file)
.with_context(|| format!("failed to read zip archive from {}", package.display()))?;
let mut entries = Vec::new();
for index in 0..archive.len() {
let file = archive.by_index(index).with_context(|| {
format!("failed to access entry {index} in {}", package.display())
})?;
entries.push(file.name().to_string());
}
entries.sort();
for entry in entries {
println!(" {}", entry);
}
}
Ok(())
}
/// Builds (if required) and tests the C# bindings against the packaged NuGet.
#[derive(Args, Clone)]
pub struct TestCsharpCommand {
/// Build the Rust FFI artefacts for the provided target triple (defaults to the host).
#[arg(long = "target", value_name = "TRIPLE")]
pub targets: Vec<String>,
/// Build and pack using the Release configuration (defaults to Debug).
#[arg(long)]
pub release: bool,
/// Remove existing build artefacts before running the command.
#[arg(long)]
pub clean: bool,
/// Override the directory that contains compiled regorus FFI artefacts.
#[arg(long, value_name = "PATH")]
pub artifacts_dir: Option<PathBuf>,
/// Require all platform artefacts to exist before packing.
#[arg(long)]
pub enforce_artifacts: bool,
/// Always rebuild the NuGet package instead of reusing an existing archive.
#[arg(long)]
pub force_nuget: bool,
/// Restore and test using Regorus NuGet artefacts located at DIR. Defaults to bindings/csharp/Regorus/bin/<configuration>.
#[arg(long = "nuget-dir", value_name = "DIR")]
pub nuget_dir: Option<PathBuf>,
}
impl TestCsharpCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let configuration = if self.release { "Release" } else { "Debug" };
if self.nuget_dir.is_some() && self.force_nuget {
return Err(anyhow!(
"--force-nuget cannot be combined with --nuget-dir; build outputs always land in the default directory"
));
}
let mut package_dir = if let Some(dir) = &self.nuget_dir {
let path = PathBuf::from(dir);
if path.is_absolute() {
path
} else {
workspace.join(path)
}
} else {
workspace
.join("bindings/csharp/Regorus/bin")
.join(configuration)
};
let build_config = BuildNugetConfig {
targets: self.targets.clone(),
release: self.release,
clean: self.clean,
artifacts_dir: self.artifacts_dir.clone(),
enforce_artifacts: self.enforce_artifacts,
};
let mut packages = find_packages(&package_dir)?;
if self.force_nuget || (packages.is_empty() && self.nuget_dir.is_none()) {
println!(
"{} NuGet package(s); invoking build...",
if self.force_nuget {
"Forcing rebuild of"
} else {
"Missing"
}
);
let build = build_nuget_package(&build_config)?;
package_dir = build.package_dir;
packages = build.packages;
} else {
println!(
"Reusing existing NuGet package(s) under {}.",
package_dir.display()
);
}
if packages.is_empty() {
return Err(anyhow!(
"No NuGet packages are available under {}; supply --nuget-dir with a populated directory or omit it to let xtask build the package",
package_dir.display()
));
}
println!("Using NuGet package(s):");
for package in &packages {
println!(" {}", package.display());
}
run_regorus_tests(&workspace, configuration, &package_dir, self.clean)?;
Ok(())
}
}
fn run_regorus_tests(
workspace: &Path,
configuration: &str,
package_dir: &Path,
clean: bool,
) -> Result<()> {
let nuget_source = package_dir
.to_str()
.ok_or_else(|| anyhow!("NuGet directory path contains invalid UTF-8"))?;
let properties = vec![format!(
"/p:RestoreAdditionalProjectSources={}",
nuget_source
)];
let property_args: Vec<&str> = properties.iter().map(|value| value.as_str()).collect();
let regorus_tests = workspace.join("bindings/csharp/Regorus.Tests");
if clean {
clean_msbuild_project(&regorus_tests, configuration)?;
}
restore_with_source(&regorus_tests, &property_args, "Regorus.Tests")?;
let mut test = Command::new("dotnet");
test.current_dir(&regorus_tests);
test.arg("test");
test.arg("--no-restore");
test.arg("-c");
test.arg(configuration);
test.arg("--arch");
test.arg(dotnet_host_arch());
run_command(test, "dotnet test (Regorus.Tests)")?;
let test_app = workspace.join("bindings/csharp/TestApp");
if clean {
clean_msbuild_project(&test_app, configuration)?;
}
restore_with_source(&test_app, &property_args, "TestApp")?;
let mut build = Command::new("dotnet");
build.current_dir(&test_app);
build.arg("build");
build.arg("--no-restore");
build.arg("-c");
build.arg(configuration);
build.arg("--arch");
build.arg(dotnet_host_arch());
run_command(build, "dotnet build (TestApp)")?;
let mut run = Command::new("dotnet");
run.current_dir(&test_app);
run.arg("run");
run.arg("--no-build");
run.arg("--framework");
run.arg("net8.0");
run.arg("-c");
run.arg(configuration);
run.arg("--arch");
run.arg(dotnet_host_arch());
run_command(run, "dotnet run (TestApp)")?;
let target_example = workspace.join("bindings/csharp/TargetExampleApp");
if clean {
clean_msbuild_project(&target_example, configuration)?;
}
restore_with_source(&target_example, &property_args, "TargetExampleApp")?;
let mut build_example = Command::new("dotnet");
build_example.current_dir(&target_example);
build_example.arg("build");
build_example.arg("--no-restore");
build_example.arg("-c");
build_example.arg(configuration);
build_example.arg("--arch");
build_example.arg(dotnet_host_arch());
run_command(build_example, "dotnet build (TargetExampleApp)")?;
let mut run_example = Command::new("dotnet");
run_example.current_dir(&target_example);
run_example.arg("run");
run_example.arg("--no-build");
run_example.arg("--framework");
run_example.arg("net8.0");
run_example.arg("-c");
run_example.arg(configuration);
run_example.arg("--arch");
run_example.arg(dotnet_host_arch());
run_command(run_example, "dotnet run (TargetExampleApp)")?;
Ok(())
}
fn restore_with_source(project_dir: &Path, properties: &[&str], label: &str) -> Result<()> {
let mut restore = Command::new("dotnet");
restore.current_dir(project_dir);
restore.arg("restore");
restore.arg("--arch");
restore.arg(dotnet_host_arch());
for property in properties {
restore.arg(property);
}
run_command(restore, &format!("dotnet restore ({label})"))
}
fn clean_msbuild_project(project_dir: &Path, configuration: &str) -> Result<()> {
if !project_dir.exists() {
return Ok(());
}
let mut clean = Command::new("dotnet");
clean.current_dir(project_dir);
clean.arg("clean");
clean.arg("-c");
clean.arg(configuration);
clean.arg("--verbosity");
clean.arg("minimal");
run_command(clean, "dotnet clean")?;
let bin_dir = project_dir.join("bin");
if bin_dir.exists() {
fs::remove_dir_all(&bin_dir).with_context(|| {
format!(
"failed to remove bin directory at {} while cleaning",
bin_dir.display()
)
})?;
}
let obj_dir = project_dir.join("obj");
if obj_dir.exists() {
fs::remove_dir_all(&obj_dir).with_context(|| {
format!(
"failed to remove obj directory at {} while cleaning",
obj_dir.display()
)
})?;
}
Ok(())
}