Wire native voice dependencies into full Rust CI (#58719)

## What changed

- Build native voice SDK and runtime artifacts with Bazel for Linux GNU, macOS, and Windows on x64 and ARM64, then configure Cargo jobs to consume them. Keep musl builds in a separate job without these inputs.
- Validate artifact receipts against the target, source commit, and source manifest; verify SDK file hashes and relocate pkg-config metadata. Package the runtime and plugins with nextest archives and restore `CODEX_TEST_VOICE_RUNTIME` when running shards.
- Prepare native Windows voice tools for argument-comment linting and install ARM64 DotSlash on Windows ARM64. Reduce archive disk usage, bound Linux ARM64 build concurrency, and correct the JUnit artifact path.
- Recognize TLS errors wrapped directly in `io::Error`. Make TLS fallback fixtures select the native backend explicitly, allow the MCP test server's non-loopback address, and isolate skill-root tests from ancestor Git markers. Install `git` in remote test containers and filter MCP discovery assertions to POST requests.

## Testing

Add tests for relocated voice inputs, Windows ARM64 import libraries with host-native pkgconf, and rejection of mismatched receipts or corrupt SDK files. Extend untrusted-certificate coverage to an explicitly selected rustls backend.

GitOrigin-RevId: da3ff690b94906edc1f8f60a3c7feda1bf66134a
This commit is contained in:
Benjamin Carlsson
2026-11-01 15:50:50 +00:00
committed by copyberry
parent 8c811e8d93
commit e02a32cd51
17 changed files with 748 additions and 47 deletions
@@ -18,6 +18,26 @@ runs:
target: ${{ inputs.target }}
cache-scope: bazel-argument-comment-lint
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
if: ${{ runner.os == 'Windows' && inputs.buildbuddy-api-key == '' }}
with:
python-version: "3.12"
architecture: x64
- name: Prepare native Windows audio dependencies
if: ${{ runner.os == 'Windows' && inputs.buildbuddy-api-key == '' }}
uses: ./third_party/voice/cargo-ci/windows-setup
with:
target: x86_64-pc-windows-msvc
- name: Verify native Windows audio dependencies
if: ${{ runner.os == 'Windows' && inputs.buildbuddy-api-key == '' }}
shell: bash
run: |
: "${VOICE_WINDOWS_BAZEL_REPOSITORY:?Windows voice tools are required}"
: "${VOICE_WINDOWS_SYSTEM_ROOT:?Windows SystemRoot is required}"
: "${VOICE_WINDOWS_HOST_ARCH:?Windows host architecture is required}"
- name: Install Linux sandbox build dependencies
if: ${{ runner.os == 'Linux' }}
shell: bash
+18
View File
@@ -81,10 +81,28 @@ runs:
run: mkdir -p "$RUNNER_TEMP/dotslash-curl" && echo retry-all-errors > "$RUNNER_TEMP/dotslash-curl/.curlrc"
- name: Install DotSlash
if: runner.os != 'Windows' || runner.arch != 'ARM64'
uses: meta/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
env:
CURL_HOME: ${{ runner.temp }}/dotslash-curl
- name: Install native DotSlash (Windows ARM64)
if: runner.os == 'Windows' && runner.arch == 'ARM64'
shell: bash
run: |
set -euo pipefail
# DotSlash selects resource architecture from its own executable.
destination="$RUNNER_TEMP/install-dotslash-arm64/bin"
archive="$RUNNER_TEMP/dotslash-windows-arm64.v0.5.9.tar.gz"
mkdir -p "$destination"
curl --location --silent --show-error --fail --retry 5 --retry-all-errors \
https://github.com/facebook/dotslash/releases/download/v0.5.9/dotslash-windows-arm64.v0.5.9.tar.gz \
--output "$archive"
echo "43d49d3472808738993ccf95a36dda8cc2869996d9a397bb02bde1e39b16146b $archive" | sha256sum --check
tar xzf "$archive" -O --force-local > "$destination/dotslash.exe"
echo "$destination" >> "$GITHUB_PATH"
"$destination/dotslash.exe" --version
- name: Install just
uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21
with:
@@ -9,6 +9,21 @@ if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
# lint aspect can run across the compatible crate graph instead of failing the
# whole build after analysis.
bazel_lint_args+=("--skip_incompatible_explicit_targets")
if [[ -z "${BUILDBUDDY_API_KEY:-}" && -n "${VOICE_WINDOWS_BAZEL_REPOSITORY:-}" ]]; then
: "${VOICE_WINDOWS_BAZEL_REPOSITORY:?Windows voice tools are required}"
: "${VOICE_WINDOWS_SYSTEM_ROOT:?Windows SystemRoot is required}"
: "${VOICE_WINDOWS_HOST_ARCH:?Windows host architecture is required}"
bazel_lint_args+=(
"--extra_toolchains=//third_party/voice:windows_pkg_config_toolchain,//third_party/voice:windows_cmake_toolchain"
"--repo_env=BAZEL_MSVC_RUNTIME_VISUAL_STUDIO_EULA=1"
"--inject_repository=voice_windows_tools=${VOICE_WINDOWS_BAZEL_REPOSITORY}"
"--//third_party/voice:windows_installed_tools=@voice_windows_tools//:tools"
"--action_env=SystemRoot=${VOICE_WINDOWS_SYSTEM_ROOT}"
"--host_action_env=SystemRoot=${VOICE_WINDOWS_SYSTEM_ROOT}"
"--action_env=PROCESSOR_ARCHITECTURE=${VOICE_WINDOWS_HOST_ARCH}"
"--host_action_env=PROCESSOR_ARCHITECTURE=${VOICE_WINDOWS_HOST_ARCH}"
)
fi
fi
read_query_labels() {
@@ -65,6 +65,8 @@ jobs:
CODEX_CI_PROFILE: ${{ inputs.profile }}
CODEX_CI_TARGET: ${{ inputs.target }}
CARGO_INCREMENTAL: "0"
# Debug symbols for hundreds of test binaries can exhaust the archive runner's disk.
CARGO_PROFILE_CI_TEST_DEBUG: "0"
SCCACHE_CACHE_SIZE: 10G
NEXTEST_ARCHIVE_FILE: nextest-${{ inputs.artifact_id }}.tar.zst
TEST_HELPERS_ARTIFACT: nextest-test-helpers-${{ inputs.artifact_id }}
@@ -73,6 +75,15 @@ jobs:
with:
persist-credentials: false
- name: Free disk space for the macOS test archive
if: ${{ runner.os == 'macOS' && runner.arch == 'ARM64' && runner.environment == 'github-hosted' }}
shell: bash
run: |
# The macOS build does not use the hosted image's Android SDK or NDKs.
df -h "$RUNNER_TEMP"
sudo rm -rf /Users/runner/Library/Android/sdk
df -h "$RUNNER_TEMP"
- uses: ./.github/actions/setup-ci
- name: Install Linux build dependencies
@@ -165,13 +176,25 @@ jobs:
tool: nextest
version: 0.9.103
- name: Set up native voice dependencies
uses: ./third_party/voice/cargo-ci/setup
with:
target: ${{ inputs.target }}
- name: Build nextest archive
shell: bash
run: |
set -euo pipefail
archive_dir="${RUNNER_TEMP}/nextest-archive"
mkdir -p "${archive_dir}"
cargo nextest archive \
if [[ "${RUNNER_OS}" == "Linux" && "${RUNNER_ARCH}" == "ARM64" && "${CODEX_CI_TARGET}" == "aarch64-unknown-linux-gnu" ]]; then
# Bound concurrency for full GNU ARM archives on native Linux runners.
nproc
free -h
export CARGO_BUILD_JOBS=4
fi
printf '%s\n' "$CODEX_VOICE_RUNTIME_RELATIVE" > "$archive_dir/voice-runtime-path.txt"
cargo nextest --tool-config-file "codex-voice:$CODEX_VOICE_NEXTEST_CONFIG" archive \
--target "${CODEX_CI_TARGET}" \
--cargo-profile "${CODEX_CI_PROFILE}" \
--timings \
@@ -215,7 +238,7 @@ jobs:
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: nextest-archive-${{ inputs.artifact_id }}
path: ${{ runner.temp }}/nextest-archive/${{ env.NEXTEST_ARCHIVE_FILE }}
path: ${{ runner.temp }}/nextest-archive/
if-no-files-found: error
retention-days: 1
@@ -341,10 +364,16 @@ jobs:
set -euo pipefail
archive_file="${RUNNER_TEMP}/nextest-archive/${NEXTEST_ARCHIVE_FILE}"
workspace_root="$(pwd)"
extract_dir="${RUNNER_TEMP}/nextest-extracted"
mkdir -p "$extract_dir"
runtime_relative="$(cat "${RUNNER_TEMP}/nextest-archive/voice-runtime-path.txt")"
export CODEX_TEST_VOICE_RUNTIME="$extract_dir/target/$runtime_relative"
if [[ "${RUNNER_OS}" == "Windows" ]]; then
archive_file="$(cygpath -w "${archive_file}")"
workspace_root="$(cygpath -w "${workspace_root}")"
extract_dir="$(cygpath -w "$extract_dir")"
CODEX_TEST_VOICE_RUNTIME="$(cygpath -w "$CODEX_TEST_VOICE_RUNTIME")"
fi
if [[ "${RUNNER_OS}" == "Linux" ]]; then
@@ -365,6 +394,7 @@ jobs:
run
--no-fail-fast
--archive-file "${archive_file}"
--extract-to "$extract_dir"
--workspace-remap "${workspace_root}"
--partition "hash:${{ matrix.shard }}/4"
)
@@ -395,6 +425,8 @@ jobs:
"${test_command[@]}"
env:
RUST_BACKTRACE: 1
# Keep panic backtraces without changing the CLI errors asserted by tests.
RUST_LIB_BACKTRACE: "0"
RUST_MIN_STACK: "8388608" # 8 MiB
NEXTEST_STATUS_LEVEL: leak
@@ -403,7 +435,7 @@ jobs:
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: nextest-junit-rust-ci-${{ inputs.artifact_id }}-shard-${{ matrix.shard }}
path: ${{ env.CARGO_TARGET_DIR }}/nextest/default/junit.xml
path: codex-rs/target/nextest/default/junit.xml
if-no-files-found: warn
- name: Tear down remote test env
+100 -32
View File
@@ -92,7 +92,7 @@ jobs:
argument_comment_lint_prebuilt:
name: Argument comment lint - ${{ matrix.name }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
timeout-minutes: 30
timeout-minutes: 120
environment:
name: bazel
deployment: false
@@ -117,17 +117,51 @@ jobs:
uses: ./.github/actions/run-argument-comment-lint
with:
target: ${{ runner.os }}
buildbuddy-api-key: ${{ secrets.BUILDBUDDY_API_KEY }}
# --- CI to validate on different os/targets --------------------------------
cargo_voice_inputs:
name: Cargo voice inputs - ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
environment:
name: bazel
deployment: false
permissions:
contents: read
timeout-minutes: 120
strategy:
fail-fast: false
matrix:
include:
- { target: x86_64-unknown-linux-gnu, runner: ubuntu-24.04 }
- { target: aarch64-unknown-linux-gnu, runner: ubuntu-24.04-arm }
- { target: x86_64-apple-darwin, runner: macos-15-intel }
- { target: aarch64-apple-darwin, runner: macos-15 }
- target: x86_64-pc-windows-msvc
runner:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
- target: aarch64-pc-windows-msvc
runner:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: ./third_party/voice/cargo-ci/produce
with:
target: ${{ matrix.target }}
lint_build:
name: Lint/Build — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.profile == 'release' && ' (release)' || '' }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
timeout-minutes: 30
defaults:
needs: cargo_voice_inputs
if: &lint_build_if ${{ !cancelled() }}
name: &lint_build_name Lint/Build — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.profile == 'release' && ' (release)' || '' }}
runs-on: &lint_build_runner ${{ matrix.runs_on || matrix.runner }}
timeout-minutes: &lint_build_timeout 30
defaults: &lint_build_defaults
run:
working-directory: codex-rs
env:
env: &lint_build_env
# Speed up repeated builds across CI runs by caching compiled objects, except on
# arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
# mixed-architecture archives under sccache.
@@ -145,24 +179,12 @@ jobs:
- runner: macos-15-xlarge
target: x86_64-apple-darwin
profile: dev
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
profile: dev
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
profile: dev
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: dev
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-arm64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
profile: dev
@@ -189,18 +211,6 @@ jobs:
- runner: macos-15-xlarge
target: aarch64-apple-darwin
profile: release
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
profile: release
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: release
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: release
@@ -214,7 +224,7 @@ jobs:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
steps:
steps: &lint_build_steps
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
@@ -348,6 +358,12 @@ jobs:
with:
target: ${{ matrix.target }}
- name: Set up native voice dependencies
if: ${{ !endsWith(matrix.target, '-musl') }}
uses: ./third_party/voice/cargo-ci/setup
with:
target: ${{ matrix.target }}
- name: Install cargo-chef
if: ${{ matrix.profile == 'release' }}
uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
@@ -427,7 +443,46 @@ jobs:
/var/cache/apt
key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1
lint_build_musl:
if: *lint_build_if
name: *lint_build_name
runs-on: *lint_build_runner
timeout-minutes: *lint_build_timeout
defaults: *lint_build_defaults
env: *lint_build_env
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
profile: dev
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: dev
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-arm64
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
profile: release
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: release
runs_on:
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-arm64
steps: *lint_build_steps
tests_macos_aarch64:
needs: cargo_voice_inputs
if: ${{ !cancelled() }}
name: Tests — macos-15-xlarge - aarch64-apple-darwin
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
@@ -439,6 +494,8 @@ jobs:
secrets: inherit
tests_linux_x64_remote:
needs: cargo_voice_inputs
if: ${{ !cancelled() }}
name: Tests — ubuntu-24.04 - x86_64-unknown-linux-gnu (remote)
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
@@ -453,6 +510,8 @@ jobs:
secrets: inherit
tests_linux_arm64:
needs: cargo_voice_inputs
if: ${{ !cancelled() }}
name: Tests — ubuntu-24.04-arm - aarch64-unknown-linux-gnu
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
@@ -466,6 +525,8 @@ jobs:
secrets: inherit
tests_windows_x64:
needs: cargo_voice_inputs
if: ${{ !cancelled() }}
name: Tests — windows-x64 - x86_64-pc-windows-msvc
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
@@ -479,6 +540,8 @@ jobs:
secrets: inherit
tests_windows_arm64:
needs: cargo_voice_inputs
if: ${{ !cancelled() }}
name: Tests — windows-arm64 - aarch64-pc-windows-msvc
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
@@ -504,7 +567,9 @@ jobs:
cargo_shear,
argument_comment_lint_package,
argument_comment_lint_prebuilt,
cargo_voice_inputs,
lint_build,
lint_build_musl,
tests_macos_aarch64,
tests_linux_x64_remote,
tests_linux_arm64,
@@ -522,6 +587,7 @@ jobs:
echo "general: ${{ needs.general.result }}"
echo "shear : ${{ needs.cargo_shear.result }}"
echo "lint : ${{ needs.lint_build.result }}"
echo "musl : ${{ needs.lint_build_musl.result }}"
echo "test macos : ${{ needs.tests_macos_aarch64.result }}"
echo "test linux : ${{ needs.tests_linux_x64_remote.result }}"
echo "test arm64 : ${{ needs.tests_linux_arm64.result }}"
@@ -531,7 +597,9 @@ jobs:
[[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
[[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
[[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
[[ '${{ needs.cargo_voice_inputs.result }}' == 'success' ]] || { echo 'cargo_voice_inputs failed'; exit 1; }
[[ '${{ needs.lint_build.result }}' == 'success' ]] || { echo 'lint_build failed'; exit 1; }
[[ '${{ needs.lint_build_musl.result }}' == 'success' ]] || { echo 'lint_build_musl failed'; exit 1; }
[[ '${{ needs.tests_macos_aarch64.result }}' == 'success' ]] || { echo 'tests_macos_aarch64 failed'; exit 1; }
[[ '${{ needs.tests_linux_x64_remote.result }}' == 'success' ]] || { echo 'tests_linux_x64_remote failed'; exit 1; }
[[ '${{ needs.tests_linux_arm64.result }}' == 'success' ]] || { echo 'tests_linux_arm64 failed'; exit 1; }
+7 -1
View File
@@ -445,9 +445,15 @@ async fn repo_ancestry_without_project_marker_does_not_walk_parents() {
fs::create_dir_all(outer.join(".agents/skills")).expect("create outer skills");
fs::create_dir_all(cwd.join(".agents/skills")).expect("create cwd skills");
// Sandbox runners can put a .git marker above the temporary directory.
// Probe for an absent fixture marker while retaining the real ancestor search.
let config_stack = stack(vec![ConfigLayerEntry::new(
ConfigLayerSource::SessionFlags,
toml::toml! { project_root_markers = [".missing-skill-test-project"] }.into(),
)]);
let roots = repo_agents_skill_roots(
Some(Arc::clone(&LOCAL_FS)),
&stack(Vec::new()),
&config_stack,
&cwd,
/*empty_remote_roots*/ None,
)
@@ -110,13 +110,6 @@ impl RouteAwareRequestError {
if self.status() == Some(StatusCode::PROXY_AUTHENTICATION_REQUIRED) {
return Some(RouteFailureClass::ProxyAuthenticationRequired);
}
if let Self::Route(RouteAwareClientPoolError::Resolve(error)) = self
&& let Some(source) = error.get_ref()
&& source.is::<rustls::Error>()
{
return Some(RouteFailureClass::TlsError);
}
let mut source: Option<&(dyn std::error::Error + 'static)> = Some(self);
while let Some(error) = source {
if error.downcast_ref::<rustls::Error>().is_some()
@@ -127,7 +120,14 @@ impl RouteAwareRequestError {
if error.to_string() == "tunnel error: proxy authorization required" {
return Some(RouteFailureClass::ProxyAuthenticationRequired);
}
source = error.source();
// io::Error::source skips its inner error, which may itself be the TLS error.
source = match error
.downcast_ref::<io::Error>()
.and_then(io::Error::get_ref)
{
Some(inner) => Some(inner),
None => error.source(),
};
}
match self {
@@ -227,6 +227,15 @@ async fn direct_and_routed_clients_build_equivalent_requests() {
#[tokio::test]
async fn request_failures_classify_real_untrusted_certificate_handshakes() {
assert_untrusted_certificate_classified(HttpClientBuilder::new()).await;
}
#[tokio::test]
async fn request_failures_classify_real_untrusted_rustls_certificate_handshakes() {
assert_untrusted_certificate_classified(HttpClientBuilder::new().with_rustls_tls()).await;
}
async fn assert_untrusted_certificate_classified(client_builder: HttpClientBuilder) {
codex_utils_rustls_provider::ensure_rustls_crypto_provider();
let certificate = rcgen::generate_simple_self_signed(vec!["localhost".to_string()])
.expect("self-signed certificate should generate");
@@ -252,9 +261,10 @@ async fn request_failures_classify_real_untrusted_certificate_handshakes() {
.expect("TLS server connection should be created");
let _ = connection.complete_io(&mut stream);
});
let pool = RouteAwareClientPool::new_without_request_logging(
let pool = RouteAwareClientPool::with_builder(
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
ClientRouteClass::Api,
client_builder.without_request_logging(),
);
let mut request = reqwest::Request::new(
@@ -29,6 +29,13 @@ use crate::tls_backend_fallback::should_retry_with_rustls;
const PROTOCOL_VERSION_TLS_ALERT: &[u8] = &[21, 3, 3, 0, 2, 2, 70];
// Schannel ARM needs TLS 1.1 to expose the protocol-version alert; the x64 CI image
// disables TLS 1.1, so x64 and other platforms retain TLS 1.2.
#[cfg(all(windows, target_arch = "aarch64"))]
const NATIVE_TLS_TEST_VERSION: reqwest::tls::Version = reqwest::tls::Version::TLS_1_1;
#[cfg(not(all(windows, target_arch = "aarch64")))]
const NATIVE_TLS_TEST_VERSION: reqwest::tls::Version = reqwest::tls::Version::TLS_1_2;
type SuccessfulTlsFallbackServer = (
String,
TransportClient,
@@ -44,6 +51,7 @@ async fn default_pool_does_not_retry_a_native_tls_protocol_failure() {
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
ClientRouteClass::Other,
);
use_native_tls_for_direct_route(&pool);
let request = reqwest::Request::new(
Method::POST,
reqwest::Url::parse(&url).expect("valid HTTPS URL"),
@@ -75,6 +83,7 @@ async fn retries_a_native_tls_protocol_failure_once_with_rustls() {
ClientRouteClass::Other,
)
.with_tls_backend_fallback();
use_native_tls_for_direct_route(&pool);
let destination = reqwest::Url::parse(&url).expect("valid HTTPS URL");
let mut request = reqwest::Request::new(Method::POST, destination.clone());
*request.body_mut() = Some(Bytes::from_static(b"mcp-initialize").into());
@@ -116,6 +125,7 @@ async fn retries_a_native_tls_failure_after_another_request_caches_rustls() {
ClientRouteClass::Other,
)
.with_tls_backend_fallback();
use_native_tls_for_direct_route(&pool);
let destination = reqwest::Url::parse(&url).expect("valid HTTPS URL");
let (route, native_client, selected_tls_backend) = pool
.client_for_url_with_resolver(destination.as_str(), |_| async {
@@ -211,6 +221,7 @@ async fn successful_rustls_fallback_replays_the_request_and_reuses_the_destinati
ClientRouteClass::Other,
)
.with_tls_backend_fallback();
use_native_tls_for_direct_route(&pool);
let destination = reqwest::Url::parse(&url).expect("valid HTTPS URL");
let existing_destination =
reqwest::Url::parse("https://another-mcp.example.com/mcp").expect("valid HTTPS URL");
@@ -276,6 +287,7 @@ async fn retries_a_tls_protocol_failure_when_request_url_contains_certificate_ma
ClientRouteClass::Other,
)
.with_tls_backend_fallback();
use_native_tls_for_direct_route(&pool);
let mut destination = reqwest::Url::parse(&url).expect("valid HTTPS URL");
destination.set_path("/certificate/hostname/expired/revoked/mcp");
let request = reqwest::Request::new(Method::POST, destination);
@@ -307,6 +319,7 @@ async fn does_not_retry_a_non_replayable_streaming_request() {
ClientRouteClass::Other,
)
.with_tls_backend_fallback();
use_native_tls_for_direct_route(&pool);
let mut request = reqwest::Request::new(
Method::POST,
reqwest::Url::parse(&url).expect("valid HTTPS URL"),
@@ -341,6 +354,20 @@ async fn does_not_retry_a_non_replayable_streaming_request() {
);
}
fn use_native_tls_for_direct_route(pool: &RouteAwareClientPool) {
// Start with reqwest's default native backend even when the environment supplies a CA bundle.
let client = reqwest::Client::builder()
.no_proxy()
.min_tls_version(NATIVE_TLS_TEST_VERSION)
.max_tls_version(NATIVE_TLS_TEST_VERSION)
.build()
.expect("native TLS client should build");
pool.clients.lock().expect("client cache lock").insert(
OutboundProxyRoute::Direct,
TransportClient::new(client, RequestLogging::Enabled, http::HeaderMap::new()),
);
}
fn spawn_successful_tls_fallback_server() -> io::Result<SuccessfulTlsFallbackServer> {
codex_utils_rustls_provider::ensure_rustls_crypto_provider();
let CertifiedKey { cert, signing_key } =
@@ -165,6 +165,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
}
eprintln!("starting rmcp streamable http test server on http://{actual_bind_addr}/mcp");
let mut transport_config = StreamableHttpServerConfig::default();
if !actual_bind_addr.ip().is_loopback() {
transport_config
.allowed_hosts
.push(actual_bind_addr.to_string());
transport_config =
transport_config.with_allowed_origins([format!("http://{actual_bind_addr}")]);
}
let router = Router::new()
.route(
SESSION_POST_FAILURE_CONTROL_PATH,
@@ -220,7 +229,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
StreamableHttpService::new(
|| Ok(TestToolServer::new()),
Arc::new(LocalSessionManager::default()),
StreamableHttpServerConfig::default(),
transport_config,
),
)
.layer(middleware::from_fn_with_state(
@@ -165,6 +165,7 @@ async fn read_only_tool_requests_preserve_host_policy_and_caller_metadata() -> a
.await
.expect("recorded requests")
.into_iter()
.filter(|request| request.method == "POST")
.map(|request| request.body_json::<Value>().expect("JSON-RPC request"))
.filter(|body| matches!(body["method"].as_str(), Some("tools/list" | "tools/call")))
.map(|body| {
+1 -1
View File
@@ -59,7 +59,7 @@ setup_remote_env() {
--privileged \
--security-opt seccomp=unconfined \
ubuntu:24.04 sleep infinity >/dev/null
if ! docker exec "${container_name}" sh -lc "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y python3 zsh bubblewrap"; then
if ! docker exec "${container_name}" sh -lc "apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y python3 zsh bubblewrap git"; then
docker rm -f "${container_name}" >/dev/null 2>&1 || true
return 1
fi
+89
View File
@@ -0,0 +1,89 @@
name: Build Cargo voice inputs
description: Build and upload same-commit native voice SDK and runtime inputs.
inputs:
target:
description: Native target triple matching the runner
required: true
windows-tools:
description: Directory containing the public Windows voice bootstrap
default: .github/scripts
buildbuddy-api-key:
description: Optional BuildBuddy credential for trusted CI builds
default: ""
runs:
using: composite
steps:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
architecture: ${{ contains(inputs.target, 'aarch64') && 'arm64' || 'x64' }}
- uses: ./.github/actions/prepare-bazel-ci
with:
target: ${{ inputs.target }}
cache-scope: cargo-voice-native
- name: Set up Windows voice build tools
if: runner.os == 'Windows'
uses: ./third_party/voice/cargo-ci/windows-setup
with:
target: ${{ inputs.target }}
windows-tools: ${{ inputs.windows-tools }}
- name: Build and stage native dependencies
shell: bash
env:
BUILDBUDDY_API_KEY: ${{ inputs.buildbuddy-api-key }}
TARGET: ${{ inputs.target }}
run: |
set -euo pipefail
arch="${TARGET%%-*}"
case "$RUNNER_OS" in
Windows) prefix="windows_$arch"; temp="$(cygpath -u "$RUNNER_TEMP")" ;;
macOS) prefix="macos_$arch"; temp="$RUNNER_TEMP" ;;
Linux) prefix="linux_$arch"; temp="$RUNNER_TEMP" ;;
*) echo "Unsupported runner OS: $RUNNER_OS" >&2; exit 1 ;;
esac
wrapper=(--remote-download-toplevel --print-failed-action-summary)
flags=(--config=release --build_metadata="COMMIT_SHA=$GITHUB_SHA")
if [[ "$RUNNER_OS" == Windows ]]; then
wrapper+=(--windows-msvc-host-platform)
flags+=(--platforms=//:local_windows_msvc
--extra_toolchains=@local_config_cc//:cc-toolchain-x64_windows,@local_config_cc//:cc-toolchain-arm64_windows,//third_party/voice:windows_pkg_config_toolchain,//third_party/voice:windows_cmake_toolchain
--repo_env=BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=0 --repo_env=BAZEL_MSVC_RUNTIME_VISUAL_STUDIO_EULA=1
--inject_repository="voice_windows_tools=$VOICE_WINDOWS_BAZEL_REPOSITORY"
--//third_party/voice:windows_installed_tools=@voice_windows_tools//:tools
--action_env="SystemRoot=$VOICE_WINDOWS_SYSTEM_ROOT" --host_action_env="SystemRoot=$VOICE_WINDOWS_SYSTEM_ROOT"
--action_env="PROCESSOR_ARCHITECTURE=$VOICE_WINDOWS_HOST_ARCH" --host_action_env="PROCESSOR_ARCHITECTURE=$VOICE_WINDOWS_HOST_ARCH"
--workspace_status_command=./scripts/workspace-status.cmd)
elif [[ "$RUNNER_OS" == macOS ]]; then
flags+=(--macos_minimum_os=14.0)
fi
./.github/scripts/run-bazel-ci.sh "${wrapper[@]}" -- build "${flags[@]}" -- \
//third_party/voice:native_sdk //third_party/voice:native_runtime
runtime="bazel-bin/third_party/voice/native_runtime_${prefix}"
output="$temp/cargo-voice-$GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT"
python third_party/voice/cargo_ci.py stage --sdk "${runtime}_sdk" --runtime "$runtime" --output "$output" --target "$TARGET"
tar -czf "$temp/cargo-voice.tar.gz" -C "$output" sdk runtime
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: cargo-voice-${{ inputs.target }}
path: ${{ runner.temp }}/cargo-voice.tar.gz
if-no-files-found: error
overwrite: true
- name: Stage verified native pkgconf, support DLLs, and license
if: runner.os == 'Windows'
id: pkgconf
shell: pwsh
run: |
$directory = Split-Path -Parent $env:VOICE_PKG_CONFIG
$license = Join-Path $directory "COPYING"
Invoke-WebRequest -Uri "https://raw.githubusercontent.com/pkgconf/pkgconf/d97cae4e2a169916f77d9ee8a46c011b69e42a7b/COPYING" -OutFile $license
if ((Get-FileHash $license -Algorithm SHA256).Hash.ToLowerInvariant() -ne "055a265b839672a62f590869b0a1f8e4f5147e8f073c9ba77b17b8d7ad653966") {
throw "Pinned pkgconf license mismatch"
}
"directory=$directory" | Out-File $env:GITHUB_OUTPUT -Encoding utf8 -Append
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: runner.os == 'Windows'
with:
name: cargo-voice-pkgconf-${{ runner.arch }}
path: ${{ steps.pkgconf.outputs.directory }}
if-no-files-found: error
overwrite: true
+62
View File
@@ -0,0 +1,62 @@
name: Set up Cargo voice inputs
description: Use the target SDK and prepared runtime from this workflow run.
inputs:
target:
description: Cargo target triple
required: true
runs:
using: composite
steps:
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- name: Ensure CMake is available for the Opus build
if: runner.os == 'Linux'
shell: bash
run: command -v cmake || sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends cmake
- name: Ensure host pkg-config is available
if: runner.os == 'macOS'
shell: bash
run: command -v pkg-config || brew install pkgconf
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: cargo-voice-${{ inputs.target }}
path: ${{ runner.temp }}/cargo-voice-inputs
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
if: runner.os == 'Windows'
with:
# The x64 archive builder must execute x64 pkgconf even for ARM64.
name: cargo-voice-pkgconf-${{ runner.arch }}
path: ${{ runner.temp }}/cargo-voice-pkgconf
- name: Configure target libraries for Cargo
env:
VOICE_TARGET: ${{ inputs.target }}
shell: python
run: |
import json
import os
from pathlib import Path
import subprocess
import sys
import tarfile
import tempfile
temp = Path(os.environ["RUNNER_TEMP"])
target_dir = Path(os.environ["CARGO_TARGET_DIR"])
root = Path(tempfile.mkdtemp(prefix="cargo-voice-", dir=target_dir))
with tarfile.open(temp / "cargo-voice-inputs/cargo-voice.tar.gz") as archive:
archive.extractall(root, filter="data")
if os.name == "nt":
os.environ["VOICE_PKG_CONFIG"] = str(temp / "cargo-voice-pkgconf/pkgconf.exe")
script = Path(os.environ["GITHUB_WORKSPACE"]) / "third_party/voice/cargo_ci.py"
subprocess.run([sys.executable, str(script), "configure", "--root", str(root),
"--target", os.environ["VOICE_TARGET"]], check=True)
# nextest collects linked libraries itself. Include the runtime manifest
# and plugins too so the whole runtime can move with the test binaries.
relative = (root / "runtime").relative_to(target_dir).as_posix()
config = root / "nextest.toml"
config.write_text('[profile.default]\narchive.include = [{ path = ' + json.dumps(relative)
+ ', relative-to = "target", on-missing = "error" }]\n')
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as env:
print(f"CODEX_VOICE_NEXTEST_CONFIG={config}", file=env)
print(f"CODEX_VOICE_RUNTIME_RELATIVE={relative}", file=env)
+33
View File
@@ -0,0 +1,33 @@
name: Set up Windows voice build tools
description: Prepare verified native Windows voice tools for Bazel.
inputs:
target:
description: Native Windows target triple
required: true
windows-tools:
description: Directory containing the public Windows voice bootstrap
default: .github/scripts
runs:
using: composite
steps:
- uses: ./.github/actions/setup-msvc-env
with:
target: ${{ inputs.target }}
- name: Install verified public Windows tools
shell: pwsh
env:
WINDOWS_TOOLS: ${{ inputs.windows-tools }}
TARGET: ${{ inputs.target }}
run: |
./.github/scripts/compute-bazel-windows-path.ps1
$manifest = Get-Content -Raw "$env:WINDOWS_TOOLS/voice-cygwin-snapshot.json" | ConvertFrom-Json
$pin = $manifest.archive
$archive = Join-Path $env:RUNNER_TEMP $pin.name
Invoke-WebRequest "https://github.com/openai/codex/releases/download/$($pin.tag)/$($pin.name)" -OutFile $archive
& "$env:WINDOWS_TOOLS/setup-voice-windows.ps1" -Target $env:TARGET -SnapshotArchive $archive
if (-not $env:SystemRoot) { throw "Windows SystemRoot is required" }
"VOICE_WINDOWS_SYSTEM_ROOT=$env:SystemRoot" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
$hostArch = $env:PROCESSOR_ARCHITEW6432
if (-not $hostArch) { $hostArch = $env:PROCESSOR_ARCHITECTURE }
if (-not $hostArch) { throw "Windows host architecture is required" }
"VOICE_WINDOWS_HOST_ARCH=$hostArch" | Out-File $env:GITHUB_ENV -Encoding utf8 -Append
+153
View File
@@ -0,0 +1,153 @@
"""Stage receipt-checked voice build inputs for Cargo and configure a shard."""
import argparse
import json
import os
from pathlib import Path
import re
import shutil
import stat
import subprocess
import sys
sys.path.insert(0, str(Path(__file__).resolve().parent))
from release_runtime import stage as stage_runtime
from runtime import digest
from sdk import MODULES
def read_receipt(path: Path, target: str, commit: str) -> dict:
receipt = json.loads(path.read_text())
if (
receipt["target"] != target
or receipt["sourceCommit"] != commit
or receipt["sourceManifestSha256"]
!= digest(Path(__file__).with_name("sources.json"))
):
raise ValueError("native inputs do not match target, commit and sources")
return receipt
def stage(sdk: Path, runtime: Path, output: Path, target: str, commit: str) -> None:
sdk, runtime, output = (
sdk.resolve(strict=True),
runtime.resolve(strict=True),
output.resolve(),
)
if any(output.is_relative_to(source) for source in (sdk, runtime)):
raise ValueError("output must be outside the native inputs")
files = read_receipt(sdk / "sdk.json", target, commit)["files"]
native = read_receipt(runtime / "runtime.json", target, commit)
library_dir = output / "runtime" / ("bin" if target.endswith("msvc") else "lib")
prepared = {
entry.get("sourceSha256"): runtime / entry["path"]
for entry in native["libraries"]
if Path(entry["path"]).parent.name == library_dir.name
}
output.mkdir(parents=True)
try:
stage_runtime(runtime, output / "runtime", target)
for entry in files:
source = (sdk / entry["path"]).resolve(strict=True)
relative = source.relative_to(sdk)
if digest(source) != entry["sha256"]:
raise ValueError(f"SDK file does not match its receipt: {relative}")
dest = output / "sdk" / relative
dest.parent.mkdir(parents=True, exist_ok=True)
if source.suffix == ".pc":
# Some transitive libraries retain the producer's absolute prefix.
dest.write_text(
re.sub(
r"^prefix=.*$",
"prefix=${pcfiledir}/../..",
source.read_text(),
flags=re.MULTILINE,
)
)
else:
shutil.copy2(source, dest)
# Link with the prepared runtime bytes and put MSVC import libraries
# beside their DLLs so nextest collects a self-contained search path.
if relative.parent == Path("lib"):
alias = library_dir / relative.name
payload = (
source if source.suffix == ".lib" else prepared.get(entry["sha256"])
)
if payload and not alias.exists():
shutil.copy2(payload, alias)
except BaseException:
# Windows cannot delete read-only files copied from Bazel.
if os.name == "nt":
for copied in output.rglob("*"):
copied.chmod(copied.stat().st_mode | stat.S_IWRITE)
shutil.rmtree(output)
raise
def environment(root: Path, target: str, pkg_config: Path) -> dict[str, str]:
root, pkg_config = root.resolve(strict=True), pkg_config.resolve(strict=True)
read_receipt(root / "runtime/runtime.json", target, os.environ["GITHUB_SHA"])
suffix = target.replace("-", "_")
values = {
f"PKG_CONFIG_{suffix}": str(pkg_config),
f"PKG_CONFIG_PATH_{suffix}": str(root / "sdk/lib/pkgconfig"),
"CODEX_TEST_VOICE_RUNTIME": str(root / "runtime"),
}
for module in MODULES:
key = re.sub(r"[^A-Z0-9]", "_", module.upper())
values[f"SYSTEM_DEPS_{key}_SEARCH_NATIVE"] = str(
root / "runtime" / ("bin" if target.endswith("msvc") else "lib")
)
if module.startswith("gstreamer"):
values[f"SYSTEM_DEPS_{key}_LDFLAGS"] = ""
if not target.endswith("windows-msvc"):
origin = "@loader_path" if target.endswith("darwin") else "$ORIGIN"
values[f"CARGO_TARGET_{suffix.upper()}_RUSTFLAGS"] = (
f"-C link-arg=-Wl,-rpath,{origin}/../lib"
)
return values
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
producer = sub.add_parser("stage")
producer.add_argument("--sdk", type=Path, required=True)
producer.add_argument("--runtime", type=Path, required=True)
producer.add_argument("--output", type=Path, required=True)
consumer = sub.add_parser("configure")
consumer.add_argument("--root", type=Path, required=True)
for command in (producer, consumer):
command.add_argument("--target", required=True)
args = parser.parse_args()
if args.command == "stage":
stage(
args.sdk, args.runtime, args.output, args.target, os.environ["GITHUB_SHA"]
)
return
pkg_config = os.environ.get("VOICE_PKG_CONFIG") or shutil.which("pkg-config")
if not pkg_config:
parser.error("pkg-config is required on the build host")
values = environment(args.root, args.target, Path(pkg_config))
# Probe only the staged SDK so a missing native module cannot resolve from
# the host. Cargo keeps its usual system search for unrelated dependencies.
probe_env = {
**os.environ,
"PKG_CONFIG_PATH": "",
"PKG_CONFIG_LIBDIR": str(args.root.resolve() / "sdk/lib/pkgconfig"),
}
subprocess.run(
[pkg_config, "--exists", *MODULES, "gstreamer-1.0 >= 1.28"],
env=probe_env,
check=True,
)
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as output:
for key, value in values.items():
output.write(f"{key}={value}\n")
if __name__ == "__main__":
main()
+158
View File
@@ -0,0 +1,158 @@
"""Exercise how full Cargo CI consumes prepared native inputs."""
import json
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from cargo_ci import environment, main, stage
from package_runtime import runtime_files
from runtime import PLUGINS, digest, required_library_paths
from sdk import MODULES
def write_files(root, contents):
records = []
for name, data in contents.items():
path = root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(data)
path.chmod(0o444) # Bazel materializes these inputs read-only.
records.append({"path": name, "sha256": digest(path)})
return records
class CargoCiTests(unittest.TestCase):
def setUp(self):
temp = tempfile.TemporaryDirectory(prefix="voice cargo ")
self.addCleanup(temp.cleanup)
self.root = Path(temp.name).resolve()
self.sdk = self.root / "sdk"
self.runtime = self.root / "runtime"
self.output = self.root / "stage"
self.commit = "a" * 40
def inputs(self, target, alias, plugin):
canonical = required_library_paths(target)[0]
plugins = [plugin.format(name) for name in PLUGINS]
metadata = (
"prefix=/producer\nName: {name}\nDescription: fixture\nVersion: 1.28\n"
)
sdk = {alias: b"original", "lib/glib-2.0/include/glibconfig.h": b"header"}
for name in MODULES:
sdk[f"lib/pkgconfig/{name}.pc"] = metadata.format(name=name).encode()
if not target.endswith("msvc"):
sdk[canonical] = sdk[alias]
sdk_files = write_files(self.sdk, sdk)
libraries = write_files(
self.runtime, {name: b"prepared" for name in [canonical, *plugins]}
)
libraries[0]["sourceSha256"] = digest(self.sdk / alias)
identity = dict(
schemaVersion=1,
target=target,
sourceCommit=self.commit,
sourceManifestSha256=digest(Path(__file__).with_name("sources.json")),
)
(self.sdk / "sdk.json").write_text(json.dumps(dict(identity, files=sdk_files)))
receipt = dict(
identity, developmentOnly=True, libraries=libraries, plugins=plugins
)
(self.runtime / "runtime.json").write_text(json.dumps(receipt))
return canonical
def test_relocated_metadata_runtime_and_cargo_probe(self):
target = "aarch64-apple-darwin"
alias = "lib/libgio-2.0.dylib"
canonical = self.inputs(target, alias, "plugins/libgst{}.dylib")
stage(self.sdk, self.runtime, self.output, target, self.commit)
moved = self.root / "moved native inputs"
self.output.rename(moved)
native = moved / "runtime"
self.assertEqual(
(native / alias).read_bytes(), (self.runtime / canonical).read_bytes()
)
self.assertEqual(
runtime_files(native, target), runtime_files(self.runtime, target)
)
host = (
os.environ.get("VOICE_PKG_CONFIG")
or shutil.which("pkg-config")
or shutil.which("pkgconf")
)
if not host:
self.skipTest("host pkg-config is required to probe relocated metadata")
pkgdir = moved / "sdk/lib/pkgconfig"
probe_env = dict(os.environ, PKG_CONFIG_PATH="", PKG_CONFIG_LIBDIR=str(pkgdir))
result = subprocess.check_output(
[host, "--variable=prefix", "gio-2.0"], env=probe_env, text=True
)
# pkgconf can escape spaces while pkg-config can print the raw path.
raw = result.strip()
words = shlex.split(raw)
self.assertEqual(
Path(words[0] if len(words) == 1 else raw).resolve(), moved / "sdk"
)
env_file = self.root / "github env"
ci_env = dict(
GITHUB_SHA=self.commit, GITHUB_ENV=str(env_file), VOICE_PKG_CONFIG=host
)
argv = ["cargo_ci", "configure", "--root", str(moved), "--target", target]
with patch.dict(os.environ, ci_env), patch.object(sys, "argv", argv):
main()
written = env_file.read_text()
values = dict(line.split("=", 1) for line in written.splitlines())
self.assertEqual(
values["PKG_CONFIG_PATH_aarch64_apple_darwin"], str(pkgdir)
)
self.assertNotIn("PKG_CONFIG_LIBDIR_aarch64_apple_darwin", values)
(pkgdir / "gio-2.0.pc").unlink()
with self.assertRaises(subprocess.CalledProcessError):
main()
self.assertEqual(env_file.read_text(), written)
def test_arm64_import_library_and_dll_with_x64_host_pkgconf(self):
target = "aarch64-pc-windows-msvc"
alias = "lib/gio-2.0.lib"
canonical = self.inputs(target, alias, "bin/gst{}.dll")
stage(self.sdk, self.runtime, self.output, target, self.commit)
native = self.output / "runtime"
self.assertEqual(
(native / "bin/gio-2.0.lib").read_bytes(), (self.sdk / alias).read_bytes()
)
self.assertEqual(
(native / canonical).read_bytes(), (self.runtime / canonical).read_bytes()
)
tool = self.root / "x64 pkgconf.exe"
tool.touch()
with patch.dict(os.environ, GITHUB_SHA=self.commit):
values = environment(self.output, target, tool)
self.assertEqual(values["PKG_CONFIG_aarch64_pc_windows_msvc"], str(tool))
self.assertEqual(
values["SYSTEM_DEPS_GSTREAMER_1_0_SEARCH_NATIVE"], str(native / "bin")
)
def test_mismatched_receipt_and_corrupt_sdk_fail_staging(self):
target = "x86_64-unknown-linux-gnu"
alias = "lib/libgio-2.0.so"
self.inputs(target, alias, "lib/gstreamer-1.0/libgst{}.so")
for triple, commit in [
(target, "b" * 40),
("aarch64-unknown-linux-gnu", self.commit),
]:
with self.assertRaisesRegex(ValueError, "do not match"):
stage(self.sdk, self.runtime, self.output, triple, commit)
source = self.sdk / alias
source.chmod(0o644)
source.write_bytes(b"corrupt")
with self.assertRaisesRegex(ValueError, "receipt"):
stage(self.sdk, self.runtime, self.output, target, self.commit)
self.assertFalse(self.output.exists())