mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
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>
This commit is contained in:
committed by
GitHub
parent
2b1434b3ac
commit
e68e852ee3
@@ -0,0 +1,484 @@
|
||||
// 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(®orus_tests, configuration)?;
|
||||
}
|
||||
restore_with_source(®orus_tests, &property_args, "Regorus.Tests")?;
|
||||
|
||||
let mut test = Command::new("dotnet");
|
||||
test.current_dir(®orus_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(())
|
||||
}
|
||||
Reference in New Issue
Block a user