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
+86
-1
@@ -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()?,
|
||||
}
|
||||
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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()))
|
||||
}
|
||||
@@ -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};
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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)")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user