diff --git a/.azure-pipelines/templates/Rust.Build.Job.yml b/.azure-pipelines/templates/Rust.Build.Job.yml index d08987272..909c4ec62 100644 --- a/.azure-pipelines/templates/Rust.Build.Job.yml +++ b/.azure-pipelines/templates/Rust.Build.Job.yml @@ -77,6 +77,8 @@ jobs: version: 'ms-prod-1.93' additionalTargets: $(triplet) workingDirectory: $(workingDirectory) + cacheOptions: + enableTargetCache: true steps: - checkout: self @@ -103,13 +105,21 @@ jobs: profile: release target: $(triplet) testCondition: "eq(variables['isX64'], 'true')" # 1ES nested template takes this as a string - enableTests: $(isX64) - publishTestResults: $(isX64) - enableClippy: $(isX64) - useNextest: $(isX64) + enableTest: $(isX64) + publishTestResults: false + enableClippy: false + useNextest: false testAllTargets: false buildAllTargets: false + # Run clippy via plain cargo so the 1ES Setup task doesn't have to + # source-install clippy-sarif (~8 min). The toolchain ships clippy + # itself; `-D warnings` keeps the build gated. + - script: cargo clippy --locked --all-targets --all-features --target $(triplet) --release -- -D warnings + displayName: Clippy + condition: and(succeeded(), eq(variables['isX64'], 'true')) + workingDirectory: $(workingDirectory) + - task: EsrpCodeSigning@5 displayName: Code Sign condition: and(eq(${{ parameters.isOfficialBuild }}, true), eq('${{ item.os }}', 'windows')) diff --git a/sdk/tests/integration/isolation-session-state-aware.test.ts b/sdk/tests/integration/isolation-session-state-aware.test.ts index 3c768f01d..49ee2c781 100644 --- a/sdk/tests/integration/isolation-session-state-aware.test.ts +++ b/sdk/tests/integration/isolation-session-state-aware.test.ts @@ -24,11 +24,11 @@ import { startSandbox, stopSandbox, } from '@microsoft/mxc-sdk'; -import { createTempDir, probeStateAwareRuntime, safeDeprovision } from './test-helpers.js'; +import { createTempDir, probeStateAwareRuntime, safeDeprovision, sandboxSkipReason } from './test-helpers.js'; const skipReason = os.platform() !== 'win32' ? 'IsolationSession is Windows-only' - : await probeStateAwareRuntime('isolation_session'); + : sandboxSkipReason ?? await probeStateAwareRuntime('isolation_session'); describe('IsolationSession state-aware lifecycle E2E', { skip: skipReason }, () => { it('runs full lifecycle: provision -> start -> exec -> stop -> deprovision', async () => { diff --git a/sdk/tests/unit/sandbox.test.ts b/sdk/tests/unit/sandbox.test.ts index f505fe5d1..b15437b18 100644 --- a/sdk/tests/unit/sandbox.test.ts +++ b/sdk/tests/unit/sandbox.test.ts @@ -6,6 +6,7 @@ import assert from 'node:assert'; import { buildSandboxPayload, createConfigFromPolicy, spawnSandbox, spawnSandboxFromConfig } from '../../src/sandbox.js'; import { resolveExecutableAndArgs } from '../../src/helper.js'; import { ContainerConfig, SandboxPolicy, SandboxingMethod } from '../../src/types.js'; +import { platformSkip } from './test-helpers.js'; describe('buildSandboxPayload', () => { const defaultPolicy: SandboxPolicy = { version: '0.4.0-alpha' }; @@ -687,7 +688,7 @@ describe('Schema 0.6.0 vocabulary', () => { }); }); -describe('resolveExecutableAndArgs (containment validation)', () => { +describe('resolveExecutableAndArgs (containment validation)', { skip: platformSkip }, () => { // Use the running node binary as a stand-in executable so the helper does // not try to discover wxc-exec on disk. The helper does not actually exec // anything; it just builds the path + args. diff --git a/sdk/tests/unit/state-aware.test.ts b/sdk/tests/unit/state-aware.test.ts index 2d7f29804..d8e782379 100644 --- a/sdk/tests/unit/state-aware.test.ts +++ b/sdk/tests/unit/state-aware.test.ts @@ -18,7 +18,7 @@ import { } from '../../src/state-aware-helper.js'; import { MxcError } from '../../src/errors.js'; import { IsolationSessionUserConfig, SandboxId } from '../../src/state-aware-types.js'; -import { fakeSpawn, testOptions } from './test-helpers.js'; +import { fakeSpawn, testOptions, platformSkip } from './test-helpers.js'; describe('buildStateAwareEnvelope', () => { it('produces a provision envelope with cross-cutting fields lifted to top-level', () => { @@ -181,7 +181,7 @@ describe('parseNonExecResponse', () => { }); }); -describe('provisionSandbox', () => { +describe('provisionSandbox', { skip: platformSkip }, () => { let activeFake: ReturnType | null = null; beforeEach(() => { activeFake = null; }); @@ -236,7 +236,7 @@ describe('provisionSandbox', () => { }); }); -describe('startSandbox', () => { +describe('startSandbox', { skip: platformSkip }, () => { afterEach(() => { _resetSpawnImpl(); }); it('infers backend from sandboxId prefix and nests configurationId under experimental', async () => { @@ -252,7 +252,7 @@ describe('startSandbox', () => { }); }); -describe('stopSandbox', () => { +describe('stopSandbox', { skip: platformSkip }, () => { afterEach(() => { _resetSpawnImpl(); }); it('builds a minimal stop envelope', async () => { @@ -277,7 +277,7 @@ describe('stopSandbox', () => { }); }); -describe('deprovisionSandbox', () => { +describe('deprovisionSandbox', { skip: platformSkip }, () => { afterEach(() => { _resetSpawnImpl(); }); it('builds a minimal deprovision envelope', async () => { @@ -290,7 +290,7 @@ describe('deprovisionSandbox', () => { }); }); -describe('execInSandboxAsync', () => { +describe('execInSandboxAsync', { skip: platformSkip }, () => { afterEach(() => { _resetSpawnImpl(); }); it('returns ExecResult on successful script run', async () => { diff --git a/sdk/tests/unit/test-helpers.ts b/sdk/tests/unit/test-helpers.ts index a00a98778..5d996b8cd 100644 --- a/sdk/tests/unit/test-helpers.ts +++ b/sdk/tests/unit/test-helpers.ts @@ -4,6 +4,13 @@ import { EventEmitter } from 'events'; import { Readable } from 'stream'; import { SandboxSpawnOptions } from '../../src/sandbox.js'; +import { getPlatformSupport } from '../../src/platform.js'; + +// Skip marker for describes that hit the binary resolver: undefined when MXC +// is supported on this host, an error string when it isn't. +export const platformSkip: string | false = !getPlatformSupport().isSupported + ? 'MXC not supported on this machine' + : false; /** * Spawn-options preset for unit tests that drive the real binary-resolver diff --git a/src/lxc/src/main.rs b/src/lxc/src/main.rs index 9664b8654..8810ba7d9 100644 --- a/src/lxc/src/main.rs +++ b/src/lxc/src/main.rs @@ -12,7 +12,7 @@ use wxc_common::models::{CodexRequest, ContainmentBackend, ScriptResponse}; use wxc_common::script_runner::{handle_dry_run_exit, ScriptRunner}; use lxc_common::lxc_runner::LxcScriptRunner; -#[cfg(feature = "hyperlight")] +#[cfg(all(feature = "hyperlight", target_arch = "x86_64"))] use wxc_common::hyperlight_runner::HyperlightScriptRunner; #[derive(Parser)] @@ -113,7 +113,7 @@ fn main() { // before config parsing so the user doesn't need a JSON file on // disk just to install. if cli.setup_hyperlight { - #[cfg(feature = "hyperlight")] + #[cfg(all(feature = "hyperlight", target_arch = "x86_64"))] { let mut logger = Logger::new(if cli.debug { Mode::Console @@ -131,7 +131,7 @@ fn main() { } } } - #[cfg(not(feature = "hyperlight"))] + #[cfg(not(all(feature = "hyperlight", target_arch = "x86_64")))] { eprintln!("Error: --setup-hyperlight requires x86_64 (Hyperlight needs KVM or WHP)"); process::exit(1); @@ -196,7 +196,7 @@ fn main() { // Hyperlight is the new embedded Hyperlight+Unikraft micro-VM. let mut runner: Box = match request.containment { ContainmentBackend::Hyperlight => { - #[cfg(feature = "hyperlight")] + #[cfg(all(feature = "hyperlight", target_arch = "x86_64"))] { if !request.experimental_enabled { eprintln!( @@ -207,7 +207,7 @@ fn main() { } Box::new(HyperlightScriptRunner::new()) } - #[cfg(not(feature = "hyperlight"))] + #[cfg(not(all(feature = "hyperlight", target_arch = "x86_64")))] { eprintln!( "Error: Hyperlight backend requires x86_64 (Hyperlight needs KVM or WHP)" diff --git a/src/wxc/src/main.rs b/src/wxc/src/main.rs index a636a38e7..acb57e1dc 100644 --- a/src/wxc/src/main.rs +++ b/src/wxc/src/main.rs @@ -13,7 +13,7 @@ use wxc_common::base_container_runner::BaseContainerRunner; use wxc_common::config_parser::{is_base_container_version, load_mxc_request, ParseError}; use wxc_common::diagnostic::DiagnosticConfig; use wxc_common::filesystem_bfs::FileSystemBfsManager; -#[cfg(feature = "hyperlight")] +#[cfg(all(feature = "hyperlight", target_arch = "x86_64"))] use wxc_common::hyperlight_runner::HyperlightScriptRunner; #[cfg(feature = "isolation_session")] use wxc_common::isolation_session_runner::IsolationSessionRunner; @@ -193,7 +193,7 @@ fn main() { // config parsing so the user doesn't need a JSON file on disk // just to install. if cli.setup_hyperlight { - #[cfg(feature = "hyperlight")] + #[cfg(all(feature = "hyperlight", target_arch = "x86_64"))] { let mut logger = Logger::new(if cli.debug { Mode::Console @@ -211,7 +211,7 @@ fn main() { } } } - #[cfg(not(feature = "hyperlight"))] + #[cfg(not(all(feature = "hyperlight", target_arch = "x86_64")))] { eprintln!("Error: --setup-hyperlight requires x86_64 (Hyperlight needs KVM or WHP)"); process::exit(1); @@ -418,7 +418,7 @@ fn main() { Box::new(NanVixScriptRunner::new()) } ContainmentBackend::Hyperlight => { - #[cfg(feature = "hyperlight")] + #[cfg(all(feature = "hyperlight", target_arch = "x86_64"))] { if !request.experimental_enabled { eprintln!( @@ -429,7 +429,7 @@ fn main() { } Box::new(HyperlightScriptRunner::new()) } - #[cfg(not(feature = "hyperlight"))] + #[cfg(not(all(feature = "hyperlight", target_arch = "x86_64")))] { eprintln!( "Error: Hyperlight backend requires x86_64 (Hyperlight needs KVM or WHP)" diff --git a/src/wxc_common/src/hyperlight_runner.rs b/src/wxc_common/src/hyperlight_runner.rs index 774e0b246..e0c14991d 100644 --- a/src/wxc_common/src/hyperlight_runner.rs +++ b/src/wxc_common/src/hyperlight_runner.rs @@ -689,6 +689,7 @@ mod tests { fn policy_rejects_denied_overlapping_allow() { let mut r = runner(); let request = CodexRequest { + script_code: "print('x')".to_string(), policy: ContainerPolicy { readwrite_paths: vec!["/tmp/x".to_string()], denied_paths: vec!["/tmp/x".to_string()], @@ -706,6 +707,7 @@ mod tests { fn policy_rejects_network_rules() { let mut r = runner(); let request = CodexRequest { + script_code: "print('x')".to_string(), policy: ContainerPolicy { allowed_hosts: vec!["example.com".to_string()], ..Default::default() @@ -722,6 +724,7 @@ mod tests { fn policy_rejects_block_default_network() { let mut r = runner(); let request = CodexRequest { + script_code: "print('x')".to_string(), policy: ContainerPolicy { default_network_policy: NetworkPolicy::Block, ..Default::default() @@ -738,6 +741,7 @@ mod tests { fn policy_rejects_working_directory() { let mut r = runner(); let request = CodexRequest { + script_code: "print('x')".to_string(), working_directory: "C:/tmp".to_string(), ..Default::default() }; diff --git a/src/wxc_common/src/lib.rs b/src/wxc_common/src/lib.rs index 28626f719..3da5ba5a2 100644 --- a/src/wxc_common/src/lib.rs +++ b/src/wxc_common/src/lib.rs @@ -5,7 +5,7 @@ pub mod config_parser; pub mod encoding; pub mod error; -#[cfg(feature = "hyperlight")] +#[cfg(all(feature = "hyperlight", target_arch = "x86_64"))] pub mod hyperlight_runner; pub mod id; pub mod logger; diff --git a/src/wxc_common/src/logger.rs b/src/wxc_common/src/logger.rs index 082e1f29e..976b6e079 100644 --- a/src/wxc_common/src/logger.rs +++ b/src/wxc_common/src/logger.rs @@ -260,20 +260,22 @@ impl Logger { /// encountered, flush the completed line(s) to the pipe sink. fn diag_accumulate(&mut self, text: &str) { #[cfg(target_os = "windows")] - if self.diag_pipe.is_none() { - return; - } - #[cfg(not(target_os = "windows"))] - return; - - self.diag_line_buf.push_str(text); + { + if self.diag_pipe.is_none() { + return; + } + self.diag_line_buf.push_str(text); - // Flush each complete line. - while let Some(newline_pos) = self.diag_line_buf.find('\n') { - let line = self.diag_line_buf[..newline_pos].to_string(); - self.diag_line_buf.drain(..=newline_pos); - self.diag_flush_line(&line); + // Flush each complete line. + while let Some(newline_pos) = self.diag_line_buf.find('\n') { + let line = self.diag_line_buf[..newline_pos].to_string(); + self.diag_line_buf.drain(..=newline_pos); + self.diag_flush_line(&line); + } } + // Non-Windows: diagnostic pipe sink isn't implemented; accept & discard. + #[cfg(not(target_os = "windows"))] + let _ = text; } /// Send one complete line to the pipe sink. @@ -290,6 +292,8 @@ impl Logger { } } } + #[cfg(not(target_os = "windows"))] + let _ = line; } /// Flush and close diagnostic sinks.