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:
Anand Krishnamoorthi
2026-01-27 07:42:21 +05:30
committed by GitHub
parent 2b1434b3ac
commit e68e852ee3
56 changed files with 3478 additions and 608 deletions
+86 -1
View File
@@ -6,7 +6,14 @@ mod tasks;
use anyhow::Result;
use clap::{Parser, Subcommand};
use tasks::{BindingsCommand, UpdateDepsCommand};
use tasks::{
BindingsCommand, BuildAllBindingsCommand, BuildFfiCommand, BuildJavaCommand, BuildNugetCommand,
BuildPythonCommand, BuildWasmCommand, CiDebugCommand, CiReleaseCommand, ClippyCommand,
FmtCommand, PrecommitCommand, PrepushCommand, TestAllBindingsCommand, TestCCommand,
TestCNoStdCommand, TestCppCommand, TestCsharpCommand, TestFfiCommand, TestGoCommand,
TestJavaCommand, TestMuslCommand, TestNoStdCommand, TestPythonCommand, TestRubyCommand,
TestWasmCommand, UpdateDepsCommand,
};
#[derive(Parser)]
#[command(author, version, about, propagate_version = true)]
@@ -19,6 +26,59 @@ struct Cli {
enum Commands {
/// Bump binding manifests to match the main regorus crate version
Bindings(BindingsCommand),
/// Build the regorus FFI crate for selected targets
#[command(name = "build-ffi", alias = "ffi")]
Ffi(BuildFfiCommand),
/// Run the release-focused CI workflow locally
CiRelease(CiReleaseCommand),
/// Run the debug-focused CI workflow locally
CiDebug(CiDebugCommand),
/// Format the workspace and all binding crates
Fmt(FmtCommand),
/// Run clippy across the workspace and all binding crates
Clippy(ClippyCommand),
/// Run the repository pre-commit validation sequence
PreCommit(PrecommitCommand),
/// Run the repository pre-push validation sequence
PrePush(PrepushCommand),
/// Build every binding artefact with opinionated defaults
BuildAllBindings(BuildAllBindingsCommand),
/// Build the Java bindings via Maven
BuildJava(BuildJavaCommand),
/// Execute the Java binding test suite
TestJava(TestJavaCommand),
/// Build the Python wheels via maturin
BuildPython(BuildPythonCommand),
/// Execute the Python binding tests
TestPython(TestPythonCommand),
/// Build the WASM bindings via wasm-pack
BuildWasm(BuildWasmCommand),
/// Execute the WASM binding smoke tests
TestWasm(TestWasmCommand),
/// Execute the FFI binding test suite
TestFfi(TestFfiCommand),
/// Execute the Go binding smoke tests
TestGo(TestGoCommand),
/// Execute all binding smoke tests
TestAllBindings(TestAllBindingsCommand),
/// Execute the MUSL cross-compilation test matrix
TestMusl(TestMuslCommand),
/// Build the ensure_no_std harness for the embedded target
TestNoStd(TestNoStdCommand),
/// Configure, build, and run the C binding sample
TestC(TestCCommand),
/// Configure, build, and run the C++ binding sample
TestCpp(TestCppCommand),
/// Configure, build, and run the no_std C binding sample
#[command(name = "test-c-no-std", alias = "test-c-nostd")]
TestCNoStd(TestCNoStdCommand),
/// Execute the Ruby binding smoke tests
TestRuby(TestRubyCommand),
/// Build and validate the C# binding via its NuGet package
TestCsharp(TestCsharpCommand),
/// Build the Regorus C# NuGet package from local artefacts
#[command(name = "build-csharp", alias = "build-nuget", alias = "nuget")]
BuildCsharp(BuildNugetCommand),
/// Update dependencies across all workspace Cargo.lock files
UpdateDeps(UpdateDepsCommand),
}
@@ -28,6 +88,31 @@ fn main() -> Result<()> {
match cli.command {
Commands::Bindings(cmd) => cmd.run()?,
Commands::Ffi(cmd) => cmd.run()?,
Commands::CiRelease(cmd) => cmd.run()?,
Commands::CiDebug(cmd) => cmd.run()?,
Commands::Fmt(cmd) => cmd.run()?,
Commands::Clippy(cmd) => cmd.run()?,
Commands::PreCommit(cmd) => cmd.run()?,
Commands::PrePush(cmd) => cmd.run()?,
Commands::BuildAllBindings(cmd) => cmd.run()?,
Commands::BuildJava(cmd) => cmd.run()?,
Commands::TestJava(cmd) => cmd.run()?,
Commands::BuildPython(cmd) => cmd.run()?,
Commands::TestPython(cmd) => cmd.run()?,
Commands::BuildWasm(cmd) => cmd.run()?,
Commands::TestWasm(cmd) => cmd.run()?,
Commands::TestFfi(cmd) => cmd.run()?,
Commands::TestGo(cmd) => cmd.run()?,
Commands::TestAllBindings(cmd) => cmd.run()?,
Commands::TestMusl(cmd) => cmd.run()?,
Commands::TestNoStd(cmd) => cmd.run()?,
Commands::TestC(cmd) => cmd.run()?,
Commands::TestCpp(cmd) => cmd.run()?,
Commands::TestCNoStd(cmd) => cmd.run()?,
Commands::TestRuby(cmd) => cmd.run()?,
Commands::TestCsharp(cmd) => cmd.run()?,
Commands::BuildCsharp(cmd) => cmd.run()?,
Commands::UpdateDeps(cmd) => cmd.run()?,
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::Result;
use clap::Args;
use super::c::{TestCCommand, TestCNoStdCommand};
use super::cpp::TestCppCommand;
use super::csharp::{build_nuget_package, BuildNugetConfig, TestCsharpCommand};
use super::ffi;
use super::java::{BuildJavaCommand, TestJavaCommand};
use super::python::{BuildPythonCommand, TestPythonCommand};
use super::wasm::{BuildWasmCommand, TestWasmCommand};
use crate::tasks::util::workspace_root;
/// Builds every published binding with opinionated defaults.
#[derive(Args, Default)]
pub struct BuildAllBindingsCommand {
/// Optimise artefacts where supported (defaults to debug builds).
#[arg(long)]
pub release: bool,
}
impl BuildAllBindingsCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let release = self.release;
let targets = ffi::resolve_targets(Vec::new())?;
ffi::build_targets(&workspace, &targets, release)?;
let ffi_dir = workspace.join("bindings/ffi/target");
let nuget_result = build_nuget_package(&BuildNugetConfig {
targets: Vec::new(),
release,
clean: false,
artifacts_dir: Some(ffi_dir.clone()),
enforce_artifacts: false,
})?;
if nuget_result.packages.is_empty() {
println!("NuGet packaging completed but no archives were produced.");
} else {
for package in &nuget_result.packages {
println!("NuGet package ready at {}", package.display());
}
}
BuildJavaCommand { skip_tests: true }.run()?;
BuildPythonCommand {
release,
target: None,
target_dir: None,
frozen: false,
}
.run()?;
BuildWasmCommand {
release,
target: "nodejs".to_string(),
out_dir: None,
}
.run()?;
println!("Completed building all bindings.");
Ok(())
}
}
/// Executes the smoke tests for all bindings in sequence.
#[derive(Args, Default)]
pub struct TestAllBindingsCommand {
/// Optimise artefacts where supported (defaults to debug builds).
#[arg(long)]
pub release: bool,
/// Python executable leveraged by the binding tests.
#[arg(long, value_name = "EXE", default_value = "python3")]
pub python: String,
/// Node.js executable leveraged by the WASM test.
#[arg(long, value_name = "EXE", default_value = "node")]
pub node: String,
}
impl TestAllBindingsCommand {
pub fn run(&self) -> Result<()> {
TestCCommand {
release: self.release,
frozen: false,
skip_ffi: false,
}
.run()?;
TestCppCommand {
release: self.release,
frozen: false,
skip_ffi: true,
}
.run()?;
TestCNoStdCommand {
release: self.release,
frozen: false,
skip_ffi: true,
}
.run()?;
TestJavaCommand {
release: self.release,
frozen: false,
}
.run()?;
TestPythonCommand {
release: self.release,
target: None,
python: self.python.clone(),
}
.run()?;
TestWasmCommand {
release: self.release,
target: "nodejs".to_string(),
out_dir: None,
node: self.node.clone(),
frozen: false,
skip_build: false,
}
.run()?;
TestCsharpCommand {
targets: Vec::new(),
release: self.release,
clean: false,
artifacts_dir: None,
enforce_artifacts: false,
force_nuget: false,
nuget_dir: None,
}
.run()?;
println!("Completed testing all bindings.");
Ok(())
}
}
+134
View File
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::tasks::util::{run_cargo_step, run_command, workspace_root};
use anyhow::{anyhow, Context, Result};
use clap::Args;
#[derive(Args, Default)]
pub struct TestCCommand {
/// Build the FFI crate in release mode before exercising the binding.
#[arg(long)]
pub release: bool,
/// Pass --frozen to the preparatory cargo invocations.
#[arg(long)]
pub frozen: bool,
/// Reuse previously built FFI artefacts instead of rebuilding.
#[arg(long)]
pub skip_ffi: bool,
}
#[derive(Args, Default)]
pub struct TestCNoStdCommand {
/// Build the FFI crate in release mode before exercising the binding.
#[arg(long)]
pub release: bool,
/// Pass --frozen to the preparatory cargo invocations.
#[arg(long)]
pub frozen: bool,
/// Reuse previously built FFI artefacts instead of rebuilding.
#[arg(long)]
pub skip_ffi: bool,
}
impl TestCCommand {
pub fn run(&self) -> Result<()> {
if !self.skip_ffi {
prepare_ffi_artifacts(self.release, self.frozen)?;
}
run_binding("bindings/c", "regorus_test", self.release)
}
}
impl TestCNoStdCommand {
pub fn run(&self) -> Result<()> {
if !self.skip_ffi {
prepare_ffi_artifacts(self.release, self.frozen)?;
}
run_binding("bindings/c-nostd", "regorus_test", self.release)
}
}
pub(super) fn run_binding(relative_dir: &str, binary_name: &str, release: bool) -> Result<()> {
let workspace = workspace_root();
let source_dir = workspace.join(relative_dir);
let build_dir = source_dir.join("build");
fs::create_dir_all(&build_dir).with_context(|| {
format!(
"failed to create build directory at {}",
build_dir.display()
)
})?;
let build_type = if release { "Release" } else { "Debug" };
let mut configure = Command::new("cmake");
configure.arg("-S").arg(&source_dir);
configure.arg("-B").arg(&build_dir);
configure.arg(format!("-DCMAKE_BUILD_TYPE={build_type}"));
run_command(configure, &format!("cmake configure ({relative_dir})"))?;
let mut build = Command::new("cmake");
build.arg("--build").arg(&build_dir);
build.arg("--config").arg(build_type);
run_command(build, &format!("cmake build ({relative_dir})"))?;
let executable = locate_executable(&build_dir, binary_name)?;
let mut run = Command::new(&executable);
run.current_dir(&build_dir);
run_command(run, &format!("{binary_name} ({relative_dir})"))?;
Ok(())
}
fn locate_executable(build_dir: &Path, binary_name: &str) -> Result<PathBuf> {
let mut candidates = Vec::new();
if cfg!(windows) {
let exe = format!("{binary_name}.exe");
candidates.push(build_dir.join(&exe));
candidates.push(build_dir.join("Release").join(&exe));
candidates.push(build_dir.join("Debug").join(&exe));
} else {
candidates.push(build_dir.join(binary_name));
candidates.push(build_dir.join("Release").join(binary_name));
candidates.push(build_dir.join("Debug").join(binary_name));
}
for candidate in candidates {
if candidate.exists() {
return Ok(candidate);
}
}
Err(anyhow!(
"failed to locate built executable '{}' under {}",
binary_name,
build_dir.display()
))
}
pub(super) fn prepare_ffi_artifacts(release: bool, frozen: bool) -> Result<()> {
let workspace = workspace_root();
let ffi_dir = workspace.join("bindings/ffi");
let mut build_args = vec!["build", "--locked"];
if release {
build_args.push("--release");
}
if frozen {
build_args.push("--frozen");
}
let build_label = if release {
"cargo build --release (bindings/ffi)"
} else {
"cargo build (bindings/ffi)"
};
run_cargo_step(&ffi_dir, build_label, build_args)
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::Result;
use clap::Args;
use super::c::{prepare_ffi_artifacts, run_binding};
#[derive(Args, Default)]
pub struct TestCppCommand {
/// Build the FFI crate in release mode before exercising the binding.
#[arg(long)]
pub release: bool,
/// Pass --frozen to the preparatory cargo invocations.
#[arg(long)]
pub frozen: bool,
/// Reuse previously built FFI artefacts instead of rebuilding.
#[arg(long)]
pub skip_ffi: bool,
}
impl TestCppCommand {
pub fn run(&self) -> Result<()> {
if !self.skip_ffi {
prepare_ffi_artifacts(self.release, self.frozen)?;
}
run_binding("bindings/cpp", "regorus_test", self.release)
}
}
+484
View File
@@ -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(&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(())
}
+172
View File
@@ -0,0 +1,172 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{anyhow, bail, Context, Result};
use clap::Args;
use crate::tasks::util::{dedup, run_cargo_step, workspace_root};
/// Builds the regorus FFI crate for the selected targets.
#[derive(Args)]
pub struct BuildFfiCommand {
/// Target triples to compile (defaults to the current host when omitted).
#[arg(long = "target", value_name = "TRIPLE")]
targets: Vec<String>,
/// Build in release mode instead of debug.
#[arg(long)]
release: bool,
}
impl BuildFfiCommand {
pub fn run(&self) -> Result<()> {
let workspace_root = workspace_root();
let targets = resolve_targets(self.targets.clone())?;
let profile = profile_dir(self.release);
build_targets(&workspace_root, &targets, self.release)?;
let base = workspace_root.join("bindings/ffi/target");
if targets.len() == 1 {
println!(
"Built FFI artefacts at {}",
artifact_path(&base, targets[0].as_str(), profile).display()
);
} else {
println!("Built FFI artefacts:");
for triple in &targets {
println!(
" {} -> {}",
triple,
artifact_path(&base, triple, profile).display()
);
}
}
Ok(())
}
}
/// Resolves the list of target triples to compile, defaulting to the host.
pub fn resolve_targets(mut targets: Vec<String>) -> Result<Vec<String>> {
if targets.is_empty() {
targets.push(detect_host_triple()?);
}
dedup(&mut targets);
Ok(targets)
}
/// Compiles the FFI crate for the supplied target triples.
pub fn build_targets(root: &Path, targets: &[String], release: bool) -> Result<()> {
for target in targets {
cargo_build(root, target, release)?;
}
Ok(())
}
/// Returns the cargo profile directory associated with the release flag.
pub fn profile_dir(release: bool) -> &'static str {
if release {
"release"
} else {
"debug"
}
}
pub fn detect_host_triple() -> Result<String> {
let output = Command::new("rustc")
.arg("-Vv")
.output()
.context("failed to invoke rustc")?;
if !output.status.success() {
bail!(
"rustc -Vv failed: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let stdout = String::from_utf8(output.stdout).context("rustc output was not valid UTF-8")?;
for line in stdout.lines() {
if let Some(rest) = line.strip_prefix("host: ") {
return Ok(rest.trim().to_string());
}
}
Err(anyhow!("failed to detect host target triple"))
}
fn cargo_build(root: &Path, target: &str, release: bool) -> Result<()> {
let dir = root.join("bindings/ffi");
let mut args = vec![
OsString::from("build"),
OsString::from("--locked"),
OsString::from("--target"),
OsString::from(target),
];
if release {
args.push(OsString::from("--release"));
}
let title = format!("cargo build (ffi:{target})");
run_cargo_step(&dir, &title, args)
}
fn artifact_path(base: &Path, target: &str, profile: &str) -> PathBuf {
base.join(target).join(profile)
}
/// Executes the FFI binding test suite.
#[derive(Args, Default)]
pub struct TestFfiCommand {
/// Build the FFI crate in release mode prior to testing.
#[arg(long)]
pub release: bool,
/// Pass --frozen to all cargo invocations.
#[arg(long)]
pub frozen: bool,
}
impl TestFfiCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let ffi_dir = workspace.join("bindings/ffi");
let mut build_args = vec![OsString::from("build"), OsString::from("--locked")];
if self.release {
build_args.push(OsString::from("--release"));
}
if self.frozen {
build_args.push(OsString::from("--frozen"));
}
let build_label = if self.release {
"cargo build --release (bindings/ffi)"
} else {
"cargo build (bindings/ffi)"
};
run_cargo_step(&ffi_dir, build_label, build_args)?;
let mut test_args = vec![OsString::from("test"), OsString::from("--locked")];
if self.release {
test_args.push(OsString::from("--release"));
}
if self.frozen {
test_args.push(OsString::from("--frozen"));
}
test_args.push(OsString::from("--features"));
test_args.push(OsString::from("contention_checks"));
run_cargo_step(
&ffi_dir,
"cargo test --features contention_checks (bindings/ffi)",
test_args,
)?;
Ok(())
}
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{anyhow, Result};
use clap::Args;
use super::c::prepare_ffi_artifacts;
use crate::tasks::util::{add_library_search_path, run_command, workspace_root};
/// Builds the Go binding sample and runs its smoke test.
#[derive(Args, Default)]
pub struct TestGoCommand {
/// Build the FFI crate in release mode before exercising the Go sample.
#[arg(long)]
pub release: bool,
/// Skip rebuilding the FFI artefacts and reuse existing outputs.
#[arg(long)]
pub skip_ffi: bool,
/// Pass --frozen to the preparatory cargo invocations.
#[arg(long)]
pub frozen: bool,
}
impl TestGoCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let ffi_dir = workspace.join("bindings/ffi");
let go_dir = workspace.join("bindings/go");
let profile = if self.release { "release" } else { "debug" };
if !self.skip_ffi {
prepare_ffi_artifacts(self.release, self.frozen)?;
}
run_go_command(&go_dir, ["mod", "tidy"], "go mod tidy (bindings/go)")?;
run_go_command(&go_dir, ["build"], "go build (bindings/go)")?;
let binary = go_test_binary(&go_dir);
if !binary.exists() {
return Err(anyhow!(
"expected Go test binary at {} after build",
binary.display()
));
}
let lib_dir = ffi_dir.join("target").join(profile);
let mut run = Command::new(&binary);
run.current_dir(&go_dir);
add_library_search_path(&mut run, &lib_dir);
run_command(run, "regorus_test (bindings/go)")
}
}
fn run_go_command<const N: usize>(dir: &Path, args: [&str; N], label: &str) -> Result<()> {
let mut cmd = Command::new("go");
cmd.current_dir(dir);
for arg in args {
cmd.arg(arg);
}
run_command(cmd, label)
}
fn go_test_binary(dir: &Path) -> PathBuf {
if cfg!(windows) {
dir.join("regorus_test.exe")
} else {
dir.join("regorus_test")
}
}
+134
View File
@@ -0,0 +1,134 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{anyhow, Context, Result};
use clap::Args;
use crate::tasks::util::{path_separator, run_cargo_step, run_command, workspace_root};
#[derive(Args, Default)]
pub struct BuildJavaCommand {
/// Skip the Maven test phase while packaging.
#[arg(long)]
pub skip_tests: bool,
}
impl BuildJavaCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let java_dir = workspace.join("bindings/java");
run_command(self.into_command(&java_dir)?, "mvn package (bindings/java)")
}
fn into_command(&self, java_dir: &Path) -> Result<Command> {
let mut mvn = Command::new("mvn");
mvn.current_dir(java_dir);
mvn.arg("--batch-mode");
mvn.arg("--no-transfer-progress");
mvn.arg("package");
if self.skip_tests {
mvn.arg("-DskipTests");
}
Ok(mvn)
}
}
#[derive(Args, Default)]
pub struct TestJavaCommand {
/// Build the JNI artefacts in release mode before testing.
#[arg(long)]
pub release: bool,
/// Propagate --frozen to cargo invocations.
#[arg(long)]
pub frozen: bool,
}
impl TestJavaCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let java_dir = workspace.join("bindings/java");
build_java_crate(&workspace, self.release, self.frozen)?;
run_command(
BuildJavaCommand { skip_tests: false }.into_command(&java_dir)?,
"mvn package (bindings/java)",
)?;
run_java_integration(&java_dir, self.release)
}
}
fn build_java_crate(workspace: &Path, release: bool, frozen: bool) -> Result<()> {
let mut args = vec!["build"];
if release {
args.push("--release");
}
if frozen {
args.push("--frozen");
}
args.push("--manifest-path");
args.push("bindings/java/Cargo.toml");
args.push("--locked");
run_cargo_step(workspace, "cargo build (bindings/java)", args)
}
fn run_java_integration(java_dir: &Path, release: bool) -> Result<()> {
let jar = locate_java_jar(java_dir)?;
let separator = path_separator();
let classpath = format!("{}{}.", jar.display(), separator);
let mut javac = Command::new("javac");
javac.current_dir(java_dir);
javac.arg("-cp");
javac.arg(&classpath);
javac.arg("Test.java");
run_command(javac, "javac Test.java (bindings/java)")?;
let profile = if release { "release" } else { "debug" };
let lib_path = java_dir.join("target").join(profile);
let mut java = Command::new("java");
java.current_dir(java_dir);
java.arg(format!("-Djava.library.path={}", lib_path.display()));
java.arg("-cp");
java.arg(&classpath);
java.arg("Test");
run_command(java, "java Test (bindings/java)")
}
fn locate_java_jar(java_dir: &Path) -> Result<PathBuf> {
let target_dir = java_dir.join("target");
let entries = fs::read_dir(&target_dir).with_context(|| {
format!(
"failed to enumerate built JARs under {}",
target_dir.display()
)
})?;
let mut candidates = Vec::new();
for entry in entries {
let entry = entry?;
let path = entry.path();
if !path.is_file() {
continue;
}
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if !file_name.starts_with("regorus-java-") || !file_name.ends_with(".jar") {
continue;
}
if file_name.contains("-sources") || file_name.contains("-javadoc") {
continue;
}
candidates.push(path);
}
candidates.sort();
candidates
.pop()
.ok_or_else(|| anyhow!("no regorus-java jar found under {}", target_dir.display()))
}
+26
View File
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
pub mod all;
pub mod c;
pub mod cpp;
pub mod csharp;
pub mod ffi;
pub mod go;
pub mod java;
pub mod python;
pub mod ruby;
pub mod version;
pub mod wasm;
pub use all::{BuildAllBindingsCommand, TestAllBindingsCommand};
pub use c::{TestCCommand, TestCNoStdCommand};
pub use cpp::TestCppCommand;
pub use csharp::{BuildNugetCommand, TestCsharpCommand};
pub use ffi::{BuildFfiCommand, TestFfiCommand};
pub use go::TestGoCommand;
pub use java::{BuildJavaCommand, TestJavaCommand};
pub use python::{BuildPythonCommand, TestPythonCommand};
pub use ruby::TestRubyCommand;
pub use version::BindingsCommand;
pub use wasm::{BuildWasmCommand, TestWasmCommand};
+283
View File
@@ -0,0 +1,283 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{anyhow, bail, Context, Result};
use clap::Args;
use crate::tasks::util::{path_separator, run_cargo_step, run_command, workspace_root};
const MIN_PYTHON_VERSION: (u32, u32) = (3, 10);
#[derive(Args, Default)]
pub struct BuildPythonCommand {
/// Optimise the wheel artefacts (defaults to debug builds).
#[arg(long)]
pub release: bool,
/// Cross-compile for the provided Rust target triple.
#[arg(long, value_name = "TRIPLE")]
pub target: Option<String>,
/// Override the directory that receives built wheel files.
#[arg(long, value_name = "PATH")]
pub target_dir: Option<PathBuf>,
/// Propagate --frozen to cargo invocations prior to packaging.
#[arg(long)]
pub frozen: bool,
}
impl BuildPythonCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let python_dir = workspace.join("bindings/python");
build_python_crate(
&python_dir,
self.release,
self.target.as_deref(),
self.frozen,
)?;
let wheel_dir = self
.target_dir
.clone()
.unwrap_or_else(|| python_dir.join("wheels"));
fs::create_dir_all(&wheel_dir).with_context(|| {
format!(
"failed to create Python wheel directory at {}",
wheel_dir.display()
)
})?;
let mut maturin = Command::new("maturin");
maturin.current_dir(&python_dir);
maturin.arg("build");
if self.release {
maturin.arg("--release");
}
if let Some(target) = &self.target {
maturin.arg("--target");
maturin.arg(target);
}
maturin.arg("--target-dir");
maturin.arg(&wheel_dir);
run_command(maturin, "maturin build (bindings/python)")
}
}
#[derive(Args, Default)]
pub struct TestPythonCommand {
/// Optimise the extension module used for testing (defaults to debug builds).
#[arg(long)]
pub release: bool,
/// Cross-compile for the provided Rust target triple.
#[arg(long, value_name = "TRIPLE")]
pub target: Option<String>,
/// Python interpreter used to run the sample and tests.
#[arg(long, value_name = "EXE", default_value = "python3")]
pub python: String,
}
impl TestPythonCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let python_dir = workspace.join("bindings/python");
let (venv_python, venv_dir) = ensure_virtual_env(&python_dir, &self.python)?;
install_testing_dependencies(&venv_python)?;
install_local_package(&python_dir, self.release, self.target.as_deref(), &venv_dir)?;
let mut sample = Command::new(&venv_python);
sample.current_dir(&python_dir);
sample.arg("test.py");
let sample_label = format!("{} test.py (bindings/python)", venv_python.display());
run_command(sample, &sample_label)?;
let mut pytest = Command::new(&venv_python);
pytest.current_dir(&python_dir);
pytest.arg("-m");
pytest.arg("pytest");
pytest.arg("test_extensions.py");
let pytest_label = format!(
"{} -m pytest test_extensions.py (bindings/python)",
venv_python.display()
);
run_command(pytest, &pytest_label)
}
}
fn install_testing_dependencies(venv_python: &Path) -> Result<()> {
let mut install = Command::new(venv_python);
install.arg("-m");
install.arg("pip");
install.arg("install");
install.arg("pytest");
let label = format!(
"{} -m pip install pytest (bindings/python)",
venv_python.display()
);
run_command(install, &label)
}
fn install_local_package(
python_dir: &Path,
release: bool,
target: Option<&str>,
venv_dir: &Path,
) -> Result<()> {
let mut maturin = Command::new("maturin");
maturin.current_dir(python_dir);
maturin.arg("develop");
if release {
maturin.arg("--release");
}
if let Some(target) = target {
maturin.arg("--target");
maturin.arg(target);
}
maturin.env("VIRTUAL_ENV", venv_dir);
let bin_dir = venv_bin_dir(venv_dir);
let mut path_value = bin_dir.clone().into_os_string();
if let Some(existing) = std::env::var_os("PATH") {
if !existing.is_empty() {
path_value.push(path_separator());
path_value.push(existing);
}
}
maturin.env("PATH", path_value);
run_command(maturin, "maturin develop (bindings/python)")
}
fn build_python_crate(
python_dir: &Path,
release: bool,
target: Option<&str>,
frozen: bool,
) -> Result<()> {
let mut args = Vec::new();
args.push(OsString::from("build"));
if release {
args.push(OsString::from("--release"));
}
if let Some(target) = target {
args.push(OsString::from("--target"));
args.push(OsString::from(target));
}
if frozen {
args.push(OsString::from("--frozen"));
}
args.push(OsString::from("--locked"));
run_cargo_step(python_dir, "cargo build (bindings/python)", args)
}
fn ensure_virtual_env(python_dir: &Path, python: &str) -> Result<(PathBuf, PathBuf)> {
ensure_python_version(std::ffi::OsStr::new(python), python)?;
let venv_dir = python_dir.join(".venv");
if venv_dir.exists() {
let existing_python = venv_bin_dir(&venv_dir).join(venv_python_name());
if existing_python.exists()
&& ensure_python_version(
existing_python.as_os_str(),
&existing_python.display().to_string(),
)
.is_ok()
{
return Ok((existing_python, venv_dir));
}
fs::remove_dir_all(&venv_dir).with_context(|| {
format!(
"failed to remove incompatible virtual environment at {}",
venv_dir.display()
)
})?;
}
let mut create = Command::new(python);
create.current_dir(python_dir);
create.arg("-m");
create.arg("venv");
create.arg(".venv");
let label = format!("{} -m venv .venv (bindings/python)", python);
run_command(create, &label)?;
let python_path = venv_bin_dir(&venv_dir).join(venv_python_name());
ensure_python_version(python_path.as_os_str(), &python_path.display().to_string())?;
Ok((python_path, venv_dir))
}
fn venv_bin_dir(venv_dir: &Path) -> PathBuf {
if cfg!(windows) {
venv_dir.join("Scripts")
} else {
venv_dir.join("bin")
}
}
fn venv_python_name() -> &'static str {
if cfg!(windows) {
"python.exe"
} else {
"python"
}
}
fn ensure_python_version(executable: &std::ffi::OsStr, label: &str) -> Result<()> {
let (major, minor) = query_python_version(executable, label)?;
if (major, minor) < MIN_PYTHON_VERSION {
bail!(
"Python interpreter {} reports version {}.{}; the bindings require Python >= {}.{}. Use --python to provide a compatible interpreter.",
label,
major,
minor,
MIN_PYTHON_VERSION.0,
MIN_PYTHON_VERSION.1
);
}
Ok(())
}
fn query_python_version(executable: &std::ffi::OsStr, label: &str) -> Result<(u32, u32)> {
let output = Command::new(executable)
.arg("-c")
.arg("import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}')")
.output()
.with_context(|| format!("failed to query Python version from {}", label))?;
if !output.status.success() {
bail!(
"{} -c 'import sys; ...' exited with status {}",
label,
output.status
);
}
let stdout = String::from_utf8(output.stdout)
.with_context(|| format!("failed to decode Python version output from {}", label))?;
let trimmed = stdout.trim();
let mut parts = trimmed.split('.');
let major = parts
.next()
.ok_or_else(|| anyhow!("missing major version in '{}'", trimmed))?
.parse::<u32>()
.with_context(|| format!("failed to parse major version from '{}'", trimmed))?;
let minor = parts
.next()
.ok_or_else(|| anyhow!("missing minor version in '{}'", trimmed))?
.parse::<u32>()
.with_context(|| format!("failed to parse minor version from '{}'", trimmed))?;
Ok((major, minor))
}
+72
View File
@@ -0,0 +1,72 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::OsString;
use std::process::Command;
use anyhow::Result;
use clap::Args;
use crate::tasks::util::{run_cargo_step, run_command, workspace_root};
/// Runs the Ruby binding smoke tests.
#[derive(Args, Default)]
pub struct TestRubyCommand {
/// Skip installing the bundler gem before running tests.
#[arg(long)]
pub skip_bundler_install: bool,
/// Skip the Rust clippy pass prior to executing the Ruby test suite.
#[arg(long)]
pub skip_clippy: bool,
/// Propagate --frozen to cargo invocations ahead of the Ruby test suite.
#[arg(long)]
pub frozen: bool,
}
impl TestRubyCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let ruby_dir = workspace.join("bindings/ruby");
if !self.skip_bundler_install {
let mut gem = Command::new("gem");
gem.current_dir(&ruby_dir);
gem.arg("install");
gem.arg("bundler");
gem.arg("--user-install");
run_command(gem, "gem install bundler (bindings/ruby)")?;
}
let mut bundle_install = Command::new("bundle");
bundle_install.current_dir(&ruby_dir);
bundle_install.arg("install");
run_command(bundle_install, "bundle install (bindings/ruby)")?;
if !self.skip_clippy {
let mut clippy_args = vec![
OsString::from("clippy"),
OsString::from("--all-targets"),
OsString::from("--no-deps"),
];
if self.frozen {
clippy_args.insert(1, OsString::from("--frozen"));
}
clippy_args.push(OsString::from("--"));
clippy_args.push(OsString::from("-Dwarnings"));
run_cargo_step(
&ruby_dir,
"cargo clippy --all-targets --no-deps -- -Dwarnings (bindings/ruby)",
clippy_args,
)?;
}
let mut rake = Command::new("bundle");
rake.current_dir(&ruby_dir);
rake.arg("exec");
rake.arg("rake");
run_command(rake, "bundle exec rake (bindings/ruby)")
}
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::Result;
use clap::Args;
use crate::tasks::util::{run_command, workspace_root};
#[derive(Args, Default)]
pub struct BuildWasmCommand {
/// Optimise the generated WASM artefacts (defaults to debug builds).
#[arg(long)]
pub release: bool,
/// Target environment passed to wasm-pack (defaults to nodejs).
#[arg(long, value_name = "TARGET", default_value = "nodejs")]
pub target: String,
/// Override the directory that receives wasm-pack outputs.
#[arg(long, value_name = "PATH")]
pub out_dir: Option<PathBuf>,
}
impl BuildWasmCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let wasm_dir = workspace.join("bindings/wasm");
build_wasm(
&wasm_dir,
self.release,
&self.target,
self.out_dir.as_deref(),
)
}
}
#[derive(Args, Default)]
pub struct TestWasmCommand {
/// Optimise the generated WASM artefacts (defaults to debug builds).
#[arg(long)]
pub release: bool,
/// Target environment passed to wasm-pack (defaults to nodejs).
#[arg(long, value_name = "TARGET", default_value = "nodejs")]
pub target: String,
/// Override the directory that receives wasm-pack outputs.
#[arg(long, value_name = "PATH")]
pub out_dir: Option<PathBuf>,
/// Node.js executable used for the sample script.
#[arg(long, value_name = "EXE", default_value = "node")]
pub node: String,
/// Accepted for compatibility with CI; ignored because wasm-pack controls builds.
#[arg(long)]
pub frozen: bool,
/// Skip rebuilding the wasm artefacts before running the sample.
#[arg(long)]
pub skip_build: bool,
}
impl TestWasmCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let wasm_dir = workspace.join("bindings/wasm");
if !self.skip_build {
build_wasm(
&wasm_dir,
self.release,
&self.target,
self.out_dir.as_deref(),
)?;
}
run_wasm_tests(&wasm_dir, self.release)?;
let mut node = Command::new(&self.node);
node.current_dir(&wasm_dir);
node.arg("test.js");
let label = format!("{} test.js (bindings/wasm)", self.node);
run_command(node, &label)
}
}
fn build_wasm(wasm_dir: &Path, release: bool, target: &str, out_dir: Option<&Path>) -> Result<()> {
let mut pack = Command::new("wasm-pack");
pack.current_dir(wasm_dir);
pack.arg("build");
pack.arg("--target");
pack.arg(target);
if release {
pack.arg("--release");
}
if let Some(out_dir) = out_dir {
pack.arg("--out-dir");
pack.arg(out_dir);
}
run_command(pack, "wasm-pack build (bindings/wasm)")
}
fn run_wasm_tests(wasm_dir: &Path, release: bool) -> Result<()> {
let mut test = Command::new("wasm-pack");
test.current_dir(wasm_dir);
test.arg("test");
test.arg("--node");
if release {
test.arg("--release");
}
run_command(test, "wasm-pack test --node (bindings/wasm)")
}
+366
View File
@@ -0,0 +1,366 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::OsString;
use std::path::Path;
use anyhow::Result;
use clap::Args;
use crate::tasks::util::{
opa_passing_arguments, run_cargo_step as util_run_cargo_step, workspace_root,
};
pub mod musl;
pub use musl::TestMuslCommand;
const DEFAULT_OPA_FEATURES: &str = "opa-testutil,serde_json/arbitrary_precision";
/// Mirrors the release-focused GitHub workflow locally.
#[derive(Args, Default)]
pub struct CiReleaseCommand {
/// Propagate --frozen to cargo invocations (match CI by passing --frozen).
#[arg(long)]
pub frozen: bool,
/// Enable the supplied feature list across build and test steps (comma separated).
#[arg(long, value_delimiter = ',', value_name = "FEATURE")]
pub features: Vec<String>,
/// Override the feature set passed to the OPA tests.
#[arg(long, value_name = "FEATURES")]
pub opa_features: Option<String>,
/// Skip the `cargo build --all-features` phase.
#[arg(long)]
pub skip_all_features_build: bool,
/// Skip the `cargo test --no-default-features` phase.
#[arg(long)]
pub skip_no_default_features_tests: bool,
/// Skip the Azure Policy feature tests.
#[arg(long)]
pub skip_azure_policy: bool,
/// Skip the Azure RBAC feature tests.
#[arg(long)]
pub skip_azure_rbac: bool,
}
/// Mirrors the debug-focused GitHub workflow locally.
#[derive(Args, Default)]
pub struct CiDebugCommand {
/// Propagate --frozen to cargo invocations (match CI by passing --frozen).
#[arg(long)]
pub frozen: bool,
}
impl CiReleaseCommand {
pub fn run(&self) -> Result<()> {
let opa_features = self
.opa_features
.clone()
.unwrap_or_else(|| DEFAULT_OPA_FEATURES.to_string());
run_ci_suite(CiSuiteConfig {
release: true,
frozen: self.frozen,
include_fmt: true,
include_run_example: true,
include_azure_policy: !self.skip_azure_policy,
include_azure_rbac: !self.skip_azure_rbac,
include_all_features_build: !self.skip_all_features_build,
include_no_default_features_tests: !self.skip_no_default_features_tests,
base_features: self.features.clone(),
opa_features,
})
}
}
impl CiDebugCommand {
pub fn run(&self) -> Result<()> {
run_ci_suite(CiSuiteConfig {
release: false,
frozen: self.frozen,
include_fmt: false,
include_run_example: false,
include_azure_policy: false,
include_azure_rbac: true,
include_all_features_build: true,
include_no_default_features_tests: true,
base_features: Vec::new(),
opa_features: DEFAULT_OPA_FEATURES.to_string(),
})
}
}
struct CiSuiteConfig {
release: bool,
frozen: bool,
include_fmt: bool,
include_run_example: bool,
include_azure_policy: bool,
include_azure_rbac: bool,
include_all_features_build: bool,
include_no_default_features_tests: bool,
base_features: Vec<String>,
opa_features: String,
}
fn run_ci_suite(config: CiSuiteConfig) -> Result<()> {
let workspace = workspace_root();
let joined_features = join_features(&config.base_features);
if config.include_fmt {
run_fmt(&workspace)?;
}
if config.include_all_features_build {
run_ci_cargo_step(
&workspace,
"build",
config.release,
config.frozen,
None,
&["--all-features"],
"cargo build --all-features (ci)",
)?;
}
run_ci_cargo_step(
&workspace,
"build",
config.release,
config.frozen,
joined_features.as_deref(),
&[],
"cargo build (ci)",
)?;
if config.include_no_default_features_tests {
run_ci_cargo_step(
&workspace,
"test",
config.release,
config.frozen,
None,
&["--no-default-features"],
"cargo test --no-default-features (ci)",
)?;
}
let example_features = example_features(&config.base_features);
let example_label = format!(
"cargo build --example regorus --no-default-features --features {} (ci)",
example_features
);
run_ci_cargo_step(
&workspace,
"build",
config.release,
config.frozen,
Some(&example_features),
&["--example", "regorus", "--no-default-features"],
&example_label,
)?;
run_ci_cargo_step(
&workspace,
"test",
config.release,
config.frozen,
joined_features.as_deref(),
&["--doc"],
"cargo test --doc (ci)",
)?;
run_ci_cargo_step(
&workspace,
"test",
config.release,
config.frozen,
joined_features.as_deref(),
&[],
"cargo test (ci)",
)?;
if config.include_run_example {
run_example(
&workspace,
config.release,
config.frozen,
joined_features.as_deref(),
)?;
}
run_named_test(
&workspace,
config.release,
config.frozen,
joined_features.as_deref(),
"aci",
)?;
run_named_test(
&workspace,
config.release,
config.frozen,
joined_features.as_deref(),
"kata",
)?;
run_opa_tests(
&workspace,
config.release,
config.frozen,
&config.opa_features,
)?;
if config.include_azure_policy {
run_ci_cargo_step(
&workspace,
"test",
config.release,
config.frozen,
None,
&["--features", "azure_policy"],
"cargo test --features azure_policy (ci)",
)?;
}
if config.include_azure_rbac {
run_ci_cargo_step(
&workspace,
"test",
config.release,
config.frozen,
None,
&["--features", "azure-rbac"],
"cargo test --features azure-rbac (ci)",
)?;
}
Ok(())
}
fn join_features(features: &[String]) -> Option<String> {
if features.is_empty() {
None
} else {
Some(features.join(","))
}
}
fn example_features(base: &[String]) -> String {
let mut features = Vec::with_capacity(base.len() + 1);
features.push(String::from("std"));
features.extend(base.iter().cloned());
features.join(",")
}
fn run_fmt(workspace: &Path) -> Result<()> {
util_run_cargo_step(
workspace,
"cargo xtask fmt --check",
["xtask", "fmt", "--check"],
)
}
fn run_ci_cargo_step(
workspace: &Path,
subcommand: &str,
release: bool,
frozen: bool,
features: Option<&str>,
extra: &[&str],
label: &str,
) -> Result<()> {
let mut args = base_cargo_args(subcommand, release, frozen, features);
for arg in extra {
args.push(OsString::from(*arg));
}
util_run_cargo_step(workspace, label, args)
}
fn run_named_test(
workspace: &Path,
release: bool,
frozen: bool,
features: Option<&str>,
name: &str,
) -> Result<()> {
let label = format!("cargo test --test {} (ci)", name);
run_ci_cargo_step(
workspace,
"test",
release,
frozen,
features,
&["--test", name],
&label,
)
}
fn run_example(
workspace: &Path,
release: bool,
frozen: bool,
features: Option<&str>,
) -> Result<()> {
let mut args = base_cargo_args("run", release, frozen, features);
args.extend(
[
"--example",
"regorus",
"--",
"eval",
"-d",
"examples/server/allowed_server.rego",
"-i",
"examples/server/input.json",
"data.example",
]
.into_iter()
.map(OsString::from),
);
util_run_cargo_step(workspace, "cargo run --example regorus (ci)", args)
}
fn run_opa_tests(workspace: &Path, release: bool, frozen: bool, features: &str) -> Result<()> {
let tests = opa_passing_arguments(workspace)?;
let mut args = base_cargo_args("test", release, frozen, Some(features));
args.push(OsString::from("--test"));
args.push(OsString::from("opa"));
args.push(OsString::from("--"));
for entry in tests {
args.push(OsString::from(entry));
}
let label = format!("cargo test --test opa --features {} (ci)", features);
util_run_cargo_step(workspace, &label, args)
}
fn base_cargo_args(
subcommand: &str,
release: bool,
frozen: bool,
features: Option<&str>,
) -> Vec<OsString> {
let mut args = vec![OsString::from(subcommand)];
if release {
args.push(OsString::from("--release"));
}
if frozen {
args.push(OsString::from("--frozen"));
}
if let Some(features) = features {
if !features.is_empty() {
args.push(OsString::from("--features"));
args.push(OsString::from(features));
}
}
args
}
+150
View File
@@ -0,0 +1,150 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::OsString;
use std::path::Path;
use std::process::{Command, Stdio};
use anyhow::{anyhow, Result};
use clap::Args;
use crate::tasks::util::{
opa_passing_arguments, run_cargo_step as util_run_cargo_step, workspace_root,
};
/// Exercises the MUSL build and test matrix used in CI.
#[derive(Args, Default)]
pub struct TestMuslCommand {
/// Target triple to compile and test against.
#[arg(long, default_value = "x86_64-unknown-linux-musl")]
pub target: String,
/// Compile artefacts in release mode.
#[arg(long)]
pub release: bool,
/// Propagate --frozen to all cargo invocations.
#[arg(long)]
pub frozen: bool,
/// Feature list passed to the OPA conformance tests.
#[arg(long, default_value = "opa-testutil,serde_json/arbitrary_precision")]
pub opa_features: String,
}
impl TestMuslCommand {
pub fn run(&self) -> Result<()> {
ensure_musl_gcc()?;
let workspace = workspace_root();
run_build_all_targets(&workspace, &self.target, self.release, self.frozen)?;
run_cargo_test(&workspace, &self.target, self.release, self.frozen, &[])?;
run_named_test(&workspace, &self.target, self.release, self.frozen, "aci")?;
run_named_test(&workspace, &self.target, self.release, self.frozen, "kata")?;
run_opa_tests(
&workspace,
&self.target,
self.release,
self.frozen,
&self.opa_features,
)
}
}
fn ensure_musl_gcc() -> Result<()> {
let status = Command::new("musl-gcc")
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match status {
Ok(result) if result.success() => Ok(()),
_ => Err(anyhow!(
"musl-gcc is required but was not found in PATH. Install musl-tools to continue."
)),
}
}
fn run_build_all_targets(
workspace: &Path,
target: &str,
release: bool,
frozen: bool,
) -> Result<()> {
let mut args = cargo_target_args("build", target, release, frozen);
args.push(OsString::from("--all-targets"));
util_run_cargo_step(workspace, "cargo build --all-targets (musl)", args)
}
fn run_cargo_test(
workspace: &Path,
target: &str,
release: bool,
frozen: bool,
extra: &[&str],
) -> Result<()> {
let mut args = cargo_target_args("test", target, release, frozen);
for arg in extra {
args.push(OsString::from(*arg));
}
let mut label = String::from("cargo test (musl)");
if release {
label.push_str(" --release");
}
if !extra.is_empty() {
label.push(' ');
label.push_str(&extra.join(" "));
}
util_run_cargo_step(workspace, &label, args)
}
fn run_named_test(
workspace: &Path,
target: &str,
release: bool,
frozen: bool,
name: &str,
) -> Result<()> {
run_cargo_test(workspace, target, release, frozen, &["--test", name])
}
fn run_opa_tests(
workspace: &Path,
target: &str,
release: bool,
frozen: bool,
features: &str,
) -> Result<()> {
let tests = opa_passing_arguments(workspace)?;
let mut args = cargo_target_args("test", target, release, frozen);
args.push(OsString::from("--features"));
args.push(OsString::from(features));
args.push(OsString::from("--test"));
args.push(OsString::from("opa"));
args.push(OsString::from("--"));
for entry in tests {
args.push(OsString::from(entry));
}
let mut label = format!("cargo test --test opa --features {} (musl)", features);
if release {
label.push_str(" --release");
}
util_run_cargo_step(workspace, &label, args)
}
fn cargo_target_args(subcommand: &str, target: &str, release: bool, frozen: bool) -> Vec<OsString> {
let mut args = vec![OsString::from(subcommand)];
if release {
args.push(OsString::from("--release"));
}
if frozen {
args.push(OsString::from("--frozen"));
}
args.push(OsString::from("--locked"));
args.push(OsString::from("--target"));
args.push(OsString::from(target));
args
}
+141
View File
@@ -0,0 +1,141 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::fs::File;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use anyhow::{Context, Result};
use clap::Args;
use crate::tasks::util::{run_cargo_step, run_command, workspace_root};
const BINDING_MANIFESTS: &[&str] = &[
"bindings/ffi/Cargo.toml",
"bindings/java/Cargo.toml",
"bindings/python/Cargo.toml",
"bindings/wasm/Cargo.toml",
// TODO: Reenable this once Ruby binding is actively maintained
//"bindings/ruby/Cargo.toml",
];
/// Runs Clippy across the workspace and binding crates with CI-equivalent checks.
#[derive(Args, Default)]
pub struct ClippyCommand {
/// Emit SARIF output to the supplied path (relative to the workspace by default).
#[arg(long, value_name = "PATH")]
pub sarif: Option<PathBuf>,
}
impl ClippyCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
match self.sarif.as_ref() {
Some(sarif) => run_clippy_with_sarif(&workspace, sarif)?,
None => {
run_cargo_step(
&workspace,
"cargo clippy --all-targets --all-features -- -Dwarnings",
[
"clippy",
"--all-targets",
"--all-features",
"--",
"-Dwarnings",
],
)?;
}
}
run_cargo_step(
&workspace,
"cargo clippy --no-default-features -- -Dwarnings",
["clippy", "--no-default-features", "--", "-Dwarnings"],
)?;
for manifest in BINDING_MANIFESTS {
let label =
format!("cargo clippy --manifest-path {manifest} --all-targets -- -Dwarnings");
run_cargo_step(
&workspace,
&label,
[
"clippy",
"--manifest-path",
manifest,
"--all-targets",
"--",
"-Dwarnings",
],
)?;
}
Ok(())
}
}
fn run_clippy_with_sarif(workspace: &Path, sarif: &Path) -> Result<()> {
let sarif_path = if sarif.is_absolute() {
sarif.to_path_buf()
} else {
workspace.join(sarif)
};
if let Some(parent) = sarif_path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create SARIF directory at {}", parent.display()))?;
}
let xtask_target = workspace.join("target").join("xtask");
std::fs::create_dir_all(&xtask_target).with_context(|| {
format!(
"failed to ensure xtask scratch directory at {}",
xtask_target.display()
)
})?;
let json_path = xtask_target.join("clippy.json");
let json_file = File::create(&json_path).with_context(|| {
format!(
"failed to create intermediate clippy output at {}",
json_path.display()
)
})?;
let mut cargo = Command::new("cargo");
cargo.current_dir(workspace);
cargo.arg("clippy");
cargo.arg("--all-targets");
cargo.arg("--all-features");
cargo.arg("--message-format=json");
cargo.arg("--");
cargo.arg("-Dwarnings");
cargo.stdout(Stdio::from(json_file));
run_command(
cargo,
"cargo clippy --all-targets --all-features --message-format=json -- -Dwarnings",
)?;
let json_input = File::open(&json_path).with_context(|| {
format!(
"failed to reopen clippy JSON output at {}",
json_path.display()
)
})?;
let sarif_file = File::create(&sarif_path)
.with_context(|| format!("failed to create SARIF output at {}", sarif_path.display()))?;
let mut sarif_cmd = Command::new("clippy-sarif");
sarif_cmd.stdin(Stdio::from(json_input));
sarif_cmd.stdout(Stdio::from(sarif_file));
run_command(sarif_cmd, "clippy-sarif")?;
let mut fmt = Command::new("sarif-fmt");
fmt.arg(&sarif_path);
run_command(fmt, "sarif-fmt")?;
println!("SARIF report written to {}", sarif_path.display());
Ok(())
}
+56
View File
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use anyhow::Result;
use clap::Args;
use crate::tasks::util::{run_cargo_step, workspace_root};
const BINDING_MANIFESTS: &[&str] = &[
"bindings/ffi/Cargo.toml",
"bindings/java/Cargo.toml",
"bindings/python/Cargo.toml",
"bindings/wasm/Cargo.toml",
"bindings/ruby/Cargo.toml",
];
/// Formats every Rust crate in the repository, including binding workspaces.
#[derive(Args, Default)]
pub struct FmtCommand {
/// Fail instead of writing edits when formatting differs
#[arg(long)]
check: bool,
}
impl FmtCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let mut fmt_args = vec!["fmt", "--all"];
if self.check {
fmt_args.extend(["--", "--check"]);
}
let fmt_label = if self.check {
"cargo fmt --all -- --check"
} else {
"cargo fmt --all"
};
run_cargo_step(&workspace, fmt_label, fmt_args)?;
for manifest in BINDING_MANIFESTS {
let mut args = vec!["fmt", "--manifest-path", *manifest];
let label = if self.check {
args.extend(["--", "--check"]);
format!("cargo fmt --manifest-path {manifest} -- --check")
} else {
format!("cargo fmt --manifest-path {manifest}")
};
run_cargo_step(&workspace, &label, args)?;
}
Ok(())
}
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
pub mod clippy;
pub mod fmt;
pub mod precommit;
pub mod prepush;
pub use clippy::ClippyCommand;
pub use fmt::FmtCommand;
pub use precommit::PrecommitCommand;
pub use prepush::PrepushCommand;
+90
View File
@@ -0,0 +1,90 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::collections::HashSet;
use std::io::Write;
use std::process::{Command, Stdio};
use anyhow::{anyhow, Context, Result};
use clap::Args;
use crate::tasks::util::{log_step, run_cargo_step, workspace_root};
/// Runs the repository's pre-commit validation sequence.
#[derive(Args, Default)]
pub struct PrecommitCommand;
impl PrecommitCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
log_step("cargo build --all-targets");
run_cargo_step(
&workspace,
"cargo build --all-targets",
["build", "--all-targets"],
)
.with_context(|| "pre-commit: cargo build --all-targets failed".to_string())?;
run_cargo_step(
&workspace,
"cargo xtask fmt --check",
["xtask", "fmt", "--check"],
)
.with_context(|| "pre-commit: cargo xtask fmt --check failed".to_string())?;
run_cargo_step(&workspace, "cargo xtask clippy", ["xtask", "clippy"])
.with_context(|| "pre-commit: cargo xtask clippy failed".to_string())?;
log_step("git status --short");
verify_clean_status(&workspace)?;
Ok(())
}
}
fn verify_clean_status(root: &std::path::Path) -> Result<()> {
let mut git = Command::new("git");
git.current_dir(root);
git.arg("status");
git.arg("-s");
git.stdout(Stdio::piped());
let output = git
.output()
.with_context(|| format!("failed to inspect git status in {}", root.display()))?;
if !output.status.success() {
return Err(anyhow!(
"git status -s exited with status {}",
output.status
));
}
let stdout = String::from_utf8(output.stdout)
.with_context(|| "git status output was not UTF-8".to_string())?;
let interesting: HashSet<&str> = ["MM", "??", "AM", " M"].into_iter().collect();
let mut flagged = Vec::new();
for line in stdout.lines() {
if line.len() >= 2 {
let status = &line[..2];
if interesting.contains(status) {
flagged.push(line.to_string());
}
}
}
if flagged.is_empty() {
return Ok(());
}
let mut stderr = std::io::stderr();
writeln!(stderr, "\nUnstaged changes found:").ok();
for entry in &flagged {
writeln!(stderr, "{}", entry).ok();
}
writeln!(stderr, "Stage them and try again").ok();
Err(anyhow!("repository contains unstaged changes"))
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::path::Path;
use std::process::{Command, Stdio};
use anyhow::{Context, Result};
use clap::Args;
use super::precommit::PrecommitCommand;
use crate::tasks::util::{
log_step, opa_passing_arguments, run_cargo_step, run_command, workspace_root,
};
/// Runs the repository's pre-push validation sequence.
#[derive(Args, Default)]
pub struct PrepushCommand;
impl PrepushCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
log_step("pre-commit checks");
PrecommitCommand::default()
.run()
.with_context(|| "pre-push: pre-commit sequence failed".to_string())?;
run_cargo_step(&workspace, "cargo test --doc", ["test", "--doc"])
.with_context(|| "pre-push: cargo test --doc failed".to_string())?;
if rustup_available() {
log_step("rustup target add thumbv7m-none-eabi");
let mut rustup = Command::new("rustup");
rustup.arg("target");
rustup.arg("add");
rustup.arg("thumbv7m-none-eabi");
run_command(rustup, "rustup target add thumbv7m-none-eabi")
.with_context(|| "pre-push: rustup target add failed".to_string())?;
run_cargo_step(
&workspace,
"cargo xtask test-no-std",
["xtask", "test-no-std"],
)
.with_context(|| "pre-push: cargo xtask test-no-std failed".to_string())?;
}
run_cargo_step(
&workspace,
"cargo build --example regorus --no-default-features --features std",
[
"build",
"--example",
"regorus",
"--no-default-features",
"--features",
"std",
],
)
.with_context(|| {
"pre-push: cargo build --example regorus --no-default-features --features std failed"
.to_string()
})?;
run_cargo_step(
&workspace,
"cargo build --all-features",
["build", "--all-features"],
)
.with_context(|| "pre-push: cargo build --all-features failed".to_string())?;
run_cargo_step(&workspace, "cargo test", ["test"])
.with_context(|| "pre-push: cargo test failed".to_string())?;
run_cargo_step(
&workspace,
"cargo test --test aci",
["test", "--test", "aci"],
)
.with_context(|| "pre-push: cargo test --test aci failed".to_string())?;
run_cargo_step(
&workspace,
"cargo test --test kata",
["test", "--test", "kata"],
)
.with_context(|| "pre-push: cargo test --test kata failed".to_string())?;
run_cargo_step(
&workspace,
"cargo test --features rego-extensions",
["test", "--features", "rego-extensions"],
)
.with_context(|| "pre-push: cargo test --features rego-extensions failed".to_string())?;
run_cargo_step(
&workspace,
"cargo test --test aci --features rego-extensions",
["test", "--test", "aci", "--features", "rego-extensions"],
)
.with_context(|| {
"pre-push: cargo test --test aci --features rego-extensions failed".to_string()
})?;
run_cargo_step(
&workspace,
"cargo test --test kata --features rego-extensions",
["test", "--test", "kata", "--features", "rego-extensions"],
)
.with_context(|| {
"pre-push: cargo test --test kata --features rego-extensions failed".to_string()
})?;
log_step("cargo test --features opa-testutil,serde_json/arbitrary_precision,rego-extensions --test opa");
run_opa_conformance(&workspace).with_context(|| {
"pre-push: cargo test --features opa-testutil,serde_json/arbitrary_precision,rego-extensions --test opa failed"
.to_string()
})?;
Ok(())
}
}
fn run_opa_conformance(root: &Path) -> Result<()> {
let tests = opa_passing_arguments(root)?;
let mut cmd = Command::new("cargo");
cmd.current_dir(root);
cmd.arg("test");
cmd.arg("--features");
cmd.arg("opa-testutil,serde_json/arbitrary_precision,rego-extensions");
cmd.arg("--test");
cmd.arg("opa");
cmd.arg("--");
for entry in tests {
cmd.arg(entry);
}
run_command(
cmd,
"cargo test --features opa-testutil,serde_json/arbitrary_precision,rego-extensions --test opa",
)
}
fn rustup_available() -> bool {
let mut probe = Command::new("rustup");
probe.arg("--version");
probe.stdout(Stdio::null());
probe.stderr(Stdio::null());
probe
.status()
.map(|status| status.success())
.unwrap_or(false)
}
+13 -1
View File
@@ -2,7 +2,19 @@
// Licensed under the MIT License.
pub mod bindings;
pub mod ci;
pub mod dev;
pub mod no_std;
pub mod update_deps;
mod util;
pub use bindings::BindingsCommand;
pub use bindings::{
BindingsCommand, BuildAllBindingsCommand, BuildFfiCommand, BuildJavaCommand, BuildNugetCommand,
BuildPythonCommand, BuildWasmCommand, TestAllBindingsCommand, TestCCommand, TestCNoStdCommand,
TestCppCommand, TestCsharpCommand, TestFfiCommand, TestGoCommand, TestJavaCommand,
TestPythonCommand, TestRubyCommand, TestWasmCommand,
};
pub use ci::{CiDebugCommand, CiReleaseCommand, TestMuslCommand};
pub use dev::{ClippyCommand, FmtCommand, PrecommitCommand, PrepushCommand};
pub use no_std::TestNoStdCommand;
pub use update_deps::UpdateDepsCommand;
+46
View File
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::OsString;
use anyhow::Result;
use clap::Args;
use crate::tasks::util::{run_cargo_step, workspace_root};
/// Builds the ensure_no_std harness for an embedded target.
#[derive(Args, Default)]
pub struct TestNoStdCommand {
/// Target triple to compile (defaults to thumbv7m-none-eabi).
#[arg(long, default_value = "thumbv7m-none-eabi")]
pub target: String,
/// Compile artefacts in release mode.
#[arg(long)]
pub release: bool,
/// Propagate --frozen to the cargo invocations.
#[arg(long)]
pub frozen: bool,
}
impl TestNoStdCommand {
pub fn run(&self) -> Result<()> {
let workspace = workspace_root();
let project_dir = workspace.join("tests/ensure_no_std");
let mut args = vec![
OsString::from("build"),
OsString::from("--target"),
OsString::from(&self.target),
];
if self.release {
args.push(OsString::from("--release"));
}
if self.frozen {
args.push(OsString::from("--frozen"));
}
run_cargo_step(&project_dir, "cargo build (tests/ensure_no_std)", args)?;
Ok(())
}
}
+151
View File
@@ -0,0 +1,151 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
use std::ffi::{OsStr, OsString};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{bail, Context, Result};
/// Returns the root of the workspace that hosts the xtask crate.
pub fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("xtask resides in workspace root")
.to_path_buf()
}
/// Runs a command, surfaces the command line, and maps a failing exit status to an error.
pub fn run_command(mut command: Command, label: &str) -> Result<()> {
let display = format_command(&command);
println!("$ {}", display);
let status = command
.status()
.with_context(|| format!("failed to spawn {}", label))?;
if !status.success() {
bail!("{} failed with status {}", label, status);
}
Ok(())
}
/// Prints a simple section header to highlight the upcoming action.
pub fn log_step(title: &str) {
println!("\n=== {} ===", title);
}
/// Logs a section heading and executes a cargo subcommand.
pub fn run_cargo_step<I, S>(workspace: &Path, title: &str, args: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
run_cargo_in_step(workspace, title, args)
}
/// Logs a section heading and executes a cargo subcommand within the provided directory.
pub fn run_cargo_in_step<I, S>(directory: &Path, title: &str, args: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<OsStr>,
{
let heading = format!("Running {}", title);
log_step(&heading);
let mut command = Command::new("cargo");
command.current_dir(directory);
command.args(args);
run_command(command, title)
}
/// Formats a command with its arguments for diagnostic output.
pub fn format_command(command: &Command) -> String {
let mut parts = Vec::new();
parts.push(command.get_program().to_string_lossy().into_owned());
for arg in command.get_args() {
parts.push(arg.to_string_lossy().into_owned());
}
parts.join(" ")
}
/// Deduplicates the provided vector while preserving the original order.
pub fn dedup(values: &mut Vec<String>) {
let mut unique = Vec::new();
for value in values.drain(..) {
if unique.iter().any(|existing| existing == &value) {
continue;
}
unique.push(value);
}
*values = unique;
}
/// Returns the platform-specific path list separator character.
pub fn path_separator() -> &'static str {
if cfg!(windows) {
";"
} else {
":"
}
}
/// Returns the environment variable used for dynamic library lookup.
pub fn library_search_env_var() -> &'static str {
if cfg!(windows) {
"PATH"
} else if cfg!(target_os = "macos") {
"DYLD_LIBRARY_PATH"
} else {
"LD_LIBRARY_PATH"
}
}
/// Ensures the provided directory is prepended to the requested environment variable.
pub fn prepend_env_path(command: &mut Command, key: &str, dir: &Path) {
let mut value = OsString::new();
value.push(dir);
if let Some(existing) = std::env::var_os(key) {
if !existing.is_empty() {
value.push(path_separator());
value.push(existing);
}
}
command.env(key, value);
}
/// Prepends the supplied directory to the standard dynamic library search path.
pub fn add_library_search_path(command: &mut Command, dir: &Path) {
let key = library_search_env_var();
prepend_env_path(command, key, dir);
}
/// Returns the host architecture string expected by dotnet CLI switches.
pub fn dotnet_host_arch() -> &'static str {
match std::env::consts::ARCH {
"aarch64" => "arm64",
"x86_64" => "x64",
"arm" => "arm",
"x86" => "x86",
_ => "x64",
}
}
/// Loads the list of passing OPA tests from the repository.
pub fn opa_passing_arguments(root: &Path) -> Result<Vec<String>> {
let listing = root.join("tests/opa.passing");
let contents = fs::read_to_string(&listing).with_context(|| {
format!(
"failed to read list of passing OPA tests from {}",
listing.display()
)
})?;
Ok(contents
.split_whitespace()
.filter(|entry| !entry.is_empty())
.map(|entry| entry.to_string())
.collect())
}