diff --git a/.github/workflows/build-wheel.yml b/.github/workflows/build-wheel.yml index 57a8581f15..cfc3bef2a3 100644 --- a/.github/workflows/build-wheel.yml +++ b/.github/workflows/build-wheel.yml @@ -76,6 +76,40 @@ jobs: if: ${{ startsWith(inputs.host-platform, 'win') }} uses: ilammy/msvc-dev-cmd@v1 # TODO: ask admin to allow pinning commits + # at this point, cl.exe already exists in PATH + - name: Find cl.exe and set CL_EXE environment variable + if: ${{ startsWith(inputs.host-platform, 'win') }} + run: | + # Find the full path of cl.exe + CL_EXE=$(which cl.exe) + if [ -z "${CL_EXE}" ]; then + echo "Error: cl.exe not found in PATH" + exit 1 + fi + echo "Found cl.exe at: ${CL_EXE}" + + # This env var is passed to cibuildwheel later + echo "CL_EXE=${CL_EXE}" >> $GITHUB_ENV + + # Make the shim executable from the next step discoverable + echo "SCCACHE_WRAPPER_DIR=$(realpath .)" >> $GITHUB_ENV + + - name: Compile a shim cl.exe + if: ${{ startsWith(inputs.host-platform, 'win') }} + shell: pwsh + run: | + # Compile into cl.exe + cl /nologo /O2 .\ci\tools\cl_shim.c /Fe:cl.exe + + # Verify + if (-not (Test-Path .\cl.exe)) { + Write-Error "Failed to build cl.exe" + exit 1 + } else { + Write-Host "cl.exe shim created successfully." + } + ls + - name: Set environment variables env: CUDA_VER: ${{ inputs.cuda-version }} @@ -155,9 +189,22 @@ jobs: CIBW_ENVIRONMENT_WINDOWS: > CUDA_PATH="$(cygpath -w ${{ env.CUDA_PATH }})" CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} + SCCACHE_GHA_ENABLED=true + ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RUNTIME_URL=${{ env.ACTIONS_RUNTIME_URL }} + ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} + ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} + ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} + SCCACHE_DIR="$(cygpath -w ${{ env.SCCACHE_DIR }})" + SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} + DISTUTILS_USE_SDK=1 + PATH="$(cygpath -w ${{ env.SCCACHE_WRAPPER_DIR }});$PATH" + CL_EXE="$(cygpath -w "${{ env.CL_EXE }}")" # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > "/host/${{ env.SCCACHE_PATH }}" --show-stats + CIBW_BEFORE_TEST_WINDOWS: > + "${{ env.SCCACHE_PATH }}" --show-stats # force the test stage to be run (so that before-test is not skipped) # TODO: we might want to think twice on adding this, it does a lot of # things before reaching this command. @@ -213,9 +260,22 @@ jobs: CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_CUDA_MAJOR }} PIP_FIND_LINKS="$(cygpath -w ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }})" + SCCACHE_GHA_ENABLED=true + ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RUNTIME_URL=${{ env.ACTIONS_RUNTIME_URL }} + ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} + ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} + ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} + SCCACHE_DIR="$(cygpath -w ${{ env.SCCACHE_DIR }})" + SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} + DISTUTILS_USE_SDK=1 + PATH="$(cygpath -w ${{ env.SCCACHE_WRAPPER_DIR }});$PATH" + CL_EXE="$(cygpath -w "${{ env.CL_EXE }}")" # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > - "/host${{ env.SCCACHE_PATH }}" --show-stats + "/host/${{ env.SCCACHE_PATH }}" --show-stats + CIBW_BEFORE_TEST_WINDOWS: > + "${{ env.SCCACHE_PATH }}" --show-stats # force the test stage to be run (so that before-test is not skipped) # TODO: we might want to think twice on adding this, it does a lot of # things before reaching this command. @@ -394,9 +454,22 @@ jobs: CUDA_PYTHON_PARALLEL_LEVEL=${{ env.CUDA_PYTHON_PARALLEL_LEVEL }} CUDA_CORE_BUILD_MAJOR=${{ env.BUILD_PREV_CUDA_MAJOR }} PIP_FIND_LINKS="$(cygpath -w ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }})" + SCCACHE_GHA_ENABLED=true + ACTIONS_RUNTIME_TOKEN=${{ env.ACTIONS_RUNTIME_TOKEN }} + ACTIONS_RUNTIME_URL=${{ env.ACTIONS_RUNTIME_URL }} + ACTIONS_RESULTS_URL=${{ env.ACTIONS_RESULTS_URL }} + ACTIONS_CACHE_URL=${{ env.ACTIONS_CACHE_URL }} + ACTIONS_CACHE_SERVICE_V2=${{ env.ACTIONS_CACHE_SERVICE_V2 }} + SCCACHE_DIR="$(cygpath -w ${{ env.SCCACHE_DIR }})" + SCCACHE_CACHE_SIZE=${{ env.SCCACHE_CACHE_SIZE }} + DISTUTILS_USE_SDK=1 + PATH="$(cygpath -w ${{ env.SCCACHE_WRAPPER_DIR }});$PATH" + CL_EXE="$(cygpath -w "${{ env.CL_EXE }}")" # check cache stats before leaving cibuildwheel CIBW_BEFORE_TEST_LINUX: > - "/host${{ env.SCCACHE_PATH }}" --show-stats + "/host/${{ env.SCCACHE_PATH }}" --show-stats + CIBW_BEFORE_TEST_WINDOWS: > + "${{ env.SCCACHE_PATH }}" --show-stats # force the test stage to be run (so that before-test is not skipped) # TODO: we might want to think twice on adding this, it does a lot of # things before reaching this command. diff --git a/ci/tools/cl_shim.c b/ci/tools/cl_shim.c new file mode 100644 index 0000000000..b290109c13 --- /dev/null +++ b/ci/tools/cl_shim.c @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +/* Simple helper to append into a dynamically-growing buffer */ +static void ensure_capacity(char **buf, size_t *cap, size_t need) { + if (*cap >= need) return; + while (*cap < need) *cap *= 2; + *buf = (char*)realloc(*buf, *cap); +} + +/* Does this argument need quoting? (space, tab or double-quote) */ +static int needs_quotes(const char *s) { + for (; *s; ++s) { + if (*s == ' ' || *s == '\t' || *s == '"') return 1; + } + return 0; +} + +/* Append a single argument to the command line, quoting/escaping as needed. + This implements a simple quoting strategy sufficient for most cases: + - wrap in double quotes if contains space, tab or a double-quote + - escape internal double-quotes with backslash + Note: For full generality on Windows you may want to implement the exact + CreateProcess parsing/escaping rules; this is pragmatic and works for normal + compiler paths and file names. */ +static void append_arg(char **buf, size_t *len, size_t *cap, const char *arg) { + int quote = needs_quotes(arg); + if (quote) { + size_t need = *len + 3 + strlen(arg); /* space + quotes + content */ + ensure_capacity(buf, cap, need + 1); + (*buf)[(*len)++] = ' '; + (*buf)[(*len)++] = '"'; + for (const char *p = arg; *p; ++p) { + if (*p == '"') { + (*buf)[(*len)++] = '\\'; + (*buf)[(*len)++] = '"'; + } else { + (*buf)[(*len)++] = *p; + } + } + (*buf)[(*len)++] = '"'; + (*buf)[*len] = '\0'; + } else { + size_t need = *len + 1 + strlen(arg); + ensure_capacity(buf, cap, need + 1); + (*buf)[(*len)++] = ' '; + strcpy(*buf + *len, arg); + *len += strlen(arg); + (*buf)[*len] = '\0'; + } +} + +int main(int argc, char **argv) { + /* Determine target compiler path from CL_EXE environment variable. + If not set, fall back to "cl.exe" (will rely on PATH). */ + const char *cl_env = getenv("CL_EXE"); + const char *cl_path = cl_env && cl_env[0] ? cl_env : "cl.exe"; + + /* Build command line: sccache "" arg1 arg2 ... */ + size_t cap = 1024; + char *cmd = (char*)malloc(cap); + if (!cmd) { + fprintf(stderr, "out of memory\n"); + return 1; + } + cmd[0] = '\0'; + size_t len = 0; + /* start with 'sccache' */ + strcpy(cmd, "sccache"); + len = strlen(cmd); + + /* append the compiler path (quoted if necessary) */ + append_arg(&cmd, &len, &cap, cl_path); + + /* append the rest of the args (skip argv[0]) */ + for (int i = 1; i < argc; ++i) { + append_arg(&cmd, &len, &cap, argv[i]); + } + + /* Use CreateProcess to run the command. We pass NULL as lpApplicationName + and the built command as lpCommandLine; this lets Windows parse it. + We inherit the current environment and current working directory. */ + STARTUPINFOA si; + PROCESS_INFORMATION pi; + ZeroMemory(&si, sizeof(si)); + si.cb = sizeof(si); + ZeroMemory(&pi, sizeof(pi)); + + /* CreateProcessA expects a mutable buffer for lpCommandLine */ + if (!CreateProcessA(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) { + DWORD err = GetLastError(); + fprintf(stderr, "CreateProcess failed: %lu\n", err); + free(cmd); + return 1; + } + + /* Wait for completion and return the child exit code */ + WaitForSingleObject(pi.hProcess, INFINITE); + DWORD exit_code = 1; + if (!GetExitCodeProcess(pi.hProcess, &exit_code)) { + exit_code = 1; + } + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + free(cmd); + return (int)exit_code; +}