From 3c2bee3d502b56436ff36b1ae0cf01286d06e95e Mon Sep 17 00:00:00 2001 From: tinysec Date: Sat, 6 Jun 2026 16:07:38 +0800 Subject: [PATCH] Refactor action for maintainability --- .github/workflows/ci.yaml | 237 ++--- README.md | 260 +---- cmake/wdk7.cmake | 21 + dist/index.js | 1632 +++++++++++++++++++------------- package-lock.json | 4 +- package.json | 2 +- scripts/dismount-iso.ps1 | 4 + scripts/mount-iso.ps1 | 3 + scripts/test-e2e.ps1 | 237 +++++ scripts/test-local.ps1 | 5 + scripts/trim-dist.cjs | 63 +- src/cache.ts | 140 +++ src/debuggers.ts | 472 +++++++++ src/download.ts | 251 +++++ src/files.ts | 86 ++ src/iso.ts | 177 ++++ src/lists.ts | 21 + src/main.ts | 1067 +++------------------ src/outputs.ts | 105 ++ src/paths.ts | 121 +++ src/process.ts | 83 ++ src/settings.ts | 102 ++ src/types.ts | 53 ++ src/wdk.ts | 212 +++++ test/e2e/cmake/dbgeng.c | 16 +- test/e2e/cmake/dll.c | 18 + test/e2e/cmake/exe.c | 8 + test/e2e/cmake/fetch_dep/dep.c | 15 +- test/e2e/cmake/fetch_dep/dep.h | 4 + test/e2e/cmake/lib.c | 8 + test/e2e/cmake/native.c | 14 +- test/e2e/cmake/sys.c | 10 + test/e2e/ddkbuild/sys/driver.c | 10 + 33 files changed, 3502 insertions(+), 1959 deletions(-) create mode 100644 scripts/test-e2e.ps1 create mode 100644 src/cache.ts create mode 100644 src/debuggers.ts create mode 100644 src/download.ts create mode 100644 src/files.ts create mode 100644 src/iso.ts create mode 100644 src/lists.ts create mode 100644 src/outputs.ts create mode 100644 src/paths.ts create mode 100644 src/process.ts create mode 100644 src/settings.ts create mode 100644 src/types.ts create mode 100644 src/wdk.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b374058..15ddf29 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,204 +2,101 @@ name: ci on: workflow_dispatch: + pull_request: push: branches: - master - schedule: - - cron: "23 18 * * 0" + tags: + - "v*.*.*" permissions: contents: read jobs: - e2e: + test: + name: build and e2e runs-on: windows-2025-vs2026 timeout-minutes: 90 steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v4 - - name: build action bundle + - name: setup node + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: npm + + - name: install dependencies shell: powershell - run: | - npm.cmd ci - npm.cmd run build - git diff --exit-code -- dist/index.js + run: npm.cmd ci + + - name: build bundle + shell: powershell + run: npm.cmd run build + + - name: verify checked-in bundle + shell: powershell + run: git diff --exit-code -- dist/index.js - name: setup wdk7 - id: wdk7 uses: ./ - - name: verify default setup + - name: run e2e shell: powershell - run: | - if ("${{ steps.wdk7.outputs.found }}" -ne "true") { - throw "WDK7 was not prepared." - } - - "root=${{ steps.wdk7.outputs.root }}" - "source=${{ steps.wdk7.outputs.source }}" - "cache-hit=${{ steps.wdk7.outputs.cache-hit }}" - "dbgeng-found=${{ steps.wdk7.outputs.dbgeng-found }}" - "toolchain-file=${{ steps.wdk7.outputs.toolchain-file }}" - "ddkbuild-cmd=${{ steps.wdk7.outputs.ddkbuild-cmd }}" - - if ("${{ steps.wdk7.outputs.dbgeng-found }}" -ne "false") { - throw "DbgEng SDK should not be prepared unless debugger=true." - } - - - name: build cmake user e2e targets without debugger - shell: powershell - env: - CMAKE_GENERATOR: ${{ steps.wdk7.outputs.cmake-generator }} - WDK7_TOOLCHAIN_FILE: ${{ steps.wdk7.outputs.toolchain-file }} - run: | - $source = Join-Path $PWD "test\e2e\cmake" - $expected = @( - "e2e_cli.exe", - "e2e_dll.dll", - "e2e_lib.lib", - "e2e_native.exe" - ) - - foreach ($arch in @("i386", "amd64")) { - $build = Join-Path $PWD ".e2e\cmake-basic-$arch" - - cmake -S $source -B $build -G $env:CMAKE_GENERATOR ` - "-DCMAKE_TOOLCHAIN_FILE=$env:WDK7_TOOLCHAIN_FILE" ` - "-DWDK7_ARCH=$arch" ` - "-DCMAKE_BUILD_TYPE=Release" - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - cmake --build $build --config Release - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - foreach ($file in $expected) { - $path = Join-Path $build $file - if (-not (Test-Path -LiteralPath $path)) { - throw "Missing CMake $arch output: $path" - } - } - - $dbgengPath = Join-Path $build "e2e_dbgeng.exe" - if (Test-Path -LiteralPath $dbgengPath) { - throw "DbgEng target was built without debugger=true: $dbgengPath" - } - } + run: .\scripts\test-e2e.ps1 -Mode default - name: setup wdk7 with debugger - id: wdk7_debugger uses: ./ with: debugger: true - - name: verify debugger setup - shell: powershell - run: | - if ("${{ steps.wdk7_debugger.outputs.found }}" -ne "true") { - throw "WDK7 was not prepared." - } - if ("${{ steps.wdk7_debugger.outputs.dbgeng-found }}" -ne "true") { - throw "DbgEng SDK was not prepared with debugger=true." - } - - "debuggers-root=${{ steps.wdk7_debugger.outputs.debuggers-root }}" - "dbgeng-include-dir=${{ steps.wdk7_debugger.outputs.dbgeng-include-dir }}" - "dbgeng-lib-i386=${{ steps.wdk7_debugger.outputs.dbgeng-lib-i386 }}" - "dbgeng-lib-amd64=${{ steps.wdk7_debugger.outputs.dbgeng-lib-amd64 }}" - - - name: build cmake user e2e targets with debugger - shell: powershell - env: - CMAKE_GENERATOR: ${{ steps.wdk7_debugger.outputs.cmake-generator }} - WDK7_TOOLCHAIN_FILE: ${{ steps.wdk7_debugger.outputs.toolchain-file }} - run: | - $source = Join-Path $PWD "test\e2e\cmake" - $expected = @( - "e2e_cli.exe", - "e2e_dll.dll", - "e2e_lib.lib", - "e2e_dbgeng.exe", - "e2e_native.exe" - ) - - foreach ($arch in @("i386", "amd64")) { - $build = Join-Path $PWD ".e2e\cmake-dbgeng-$arch" - - cmake -S $source -B $build -G $env:CMAKE_GENERATOR ` - "-DCMAKE_TOOLCHAIN_FILE=$env:WDK7_TOOLCHAIN_FILE" ` - "-DWDK7_ARCH=$arch" ` - "-DCMAKE_BUILD_TYPE=Release" - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - cmake --build $build --config Release - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - foreach ($file in $expected) { - $path = Join-Path $build $file - if (-not (Test-Path -LiteralPath $path)) { - throw "Missing CMake $arch output: $path" - } - } - } - - - name: build cmake sys e2e target + - name: run debugger e2e shell: powershell + run: .\scripts\test-e2e.ps1 -Mode debugger + + release: + name: release + needs: test + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: create release env: - CMAKE_GENERATOR: ${{ steps.wdk7.outputs.cmake-generator }} - WDK7_TOOLCHAIN_FILE: ${{ steps.wdk7.outputs.toolchain-file }} + GH_TOKEN: ${{ github.token }} + TAG_NAME: ${{ github.ref_name }} run: | - $source = Join-Path $PWD "test\e2e\cmake-sys" - - foreach ($arch in @("i386", "amd64")) { - $build = Join-Path $PWD ".e2e\cmake-sys-$arch" - - cmake -S $source -B $build -G $env:CMAKE_GENERATOR ` - "-DCMAKE_TOOLCHAIN_FILE=$env:WDK7_TOOLCHAIN_FILE" ` - "-DWDK7_ARCH=$arch" ` - "-DWDK7_DEFAULT_MODE=KERNEL" ` - "-DCMAKE_BUILD_TYPE=Release" - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - cmake --build $build --config Release - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - - $path = Join-Path $build "e2e_sys.sys" - if (-not (Test-Path -LiteralPath $path)) { - throw "Missing CMake $arch sys output: $path" - } - } - - - name: build ddkbuild e2e target - shell: powershell + if ! [[ "$TAG_NAME" =~ ^v([0-9]+)\.[0-9]+\.[0-9]+$ ]]; then + echo "Skipping release for non-semver tag: $TAG_NAME" + exit 0 + fi + + if gh release view "$TAG_NAME" >/dev/null 2>&1; then + echo "Release $TAG_NAME already exists." + else + gh release create "$TAG_NAME" --verify-tag --generate-notes --title "$TAG_NAME" + fi + + - name: update major tag env: - DDKBUILD_CMD: ${{ steps.wdk7.outputs.ddkbuild-cmd }} + TAG_NAME: ${{ github.ref_name }} run: | - $source = "test\e2e\ddkbuild\sys" - - foreach ($target in @("-WIN7", "-WIN7A64")) { - cmd /s /c "call ""$env:DDKBUILD_CMD"" $target free $source" - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE - } - } - - $sysFiles = @(Get-ChildItem -Path $source -Recurse -Filter e2e_ddkbuild.sys) - if ($sysFiles.Count -lt 2) { - throw "Expected ddkbuild to produce x86 and amd64 .sys files." - } - - $sysFiles | ForEach-Object { $_.FullName } + if ! [[ "$TAG_NAME" =~ ^v([0-9]+)\.[0-9]+\.[0-9]+$ ]]; then + exit 0 + fi + + MAJOR_TAG="v${BASH_REMATCH[1]}" + TARGET_SHA="$(git rev-list -n 1 "$TAG_NAME")" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -f "$MAJOR_TAG" "$TARGET_SHA" + git push -f origin "refs/tags/$MAJOR_TAG" diff --git a/README.md b/README.md index eb9cde6..aa2cc6d 100644 --- a/README.md +++ b/README.md @@ -1,236 +1,66 @@ # setup-wdk7 -`setup-wdk7` is a GitHub/Gitea compatible JavaScript action that prepares -Windows Driver Kit 7.1 for CI jobs. - -It first detects an existing WDK7 tree, then reuses a local cache, and finally -downloads and extracts the WDK7 ISO when it cannot find a usable tree. The -default prepared environment is WDK7-only; Debugging Tools are prepared only -when `debugger: true` is set. +[![CI](https://github.com/tinysec/setup-wdk7/actions/workflows/ci.yaml/badge.svg)](https://github.com/tinysec/setup-wdk7/actions/workflows/ci.yaml) +[![Release](https://img.shields.io/github/v/release/tinysec/setup-wdk7?display_name=tag&sort=semver)](https://github.com/tinysec/setup-wdk7/releases) +[![GitHub Action](https://img.shields.io/badge/GitHub%20Action-setup--wdk7-2088ff?logo=githubactions)](https://github.com/tinysec/setup-wdk7) + +`setup-wdk7` prepares Windows Driver Kit 7.1 for legacy Windows driver and SDK +builds in CI. It finds an existing WDK7 install when available, restores a +cached copy when possible, or downloads and extracts the WDK7 ISO when the +runner starts clean. + +## Features + +- Detects WDK7 from an explicit input, `WDK7_ROOT`, `W7BASE`, local cache, or + the default `C:\WinDDK` installation path. +- Downloads, extracts, and caches WDK7 automatically on Windows runners. +- Exposes a bundled CMake toolchain for `i386` and `amd64` builds. +- Supports user-mode binaries, kernel `.sys` targets, and legacy + `ddkbuild.cmd` projects. +- Optionally prepares the Debugging Tools SDK for DbgEng and WinDbg extension + builds. ## Usage ```yaml -- name: setup wdk7 - id: wdk7 - uses: tinysec/setup-wdk7@v1 -``` - -Then use the resolved root in CMake: - -```yaml -- name: configure wdk7 - if: steps.wdk7.outputs.found == 'true' - shell: cmd - run: | - cmake -S . -B build -G "${{ steps.wdk7.outputs.cmake-generator }}" ^ - -DCMAKE_TOOLCHAIN_FILE="${{ steps.wdk7.outputs.toolchain-file }}" ^ - -DWDK7_ARCH=${{ matrix.arch }} -``` - -The action bundles `cmake/wdk7.cmake`, so projects can use the action's -toolchain file directly. If a project carries a customized copy, pass that path -to CMake instead. - -`wdk7.cmake` adapts WDK7 to ordinary CMake user-mode targets by default. It does -not provide a parallel `wdk7_add_*` helper DSL; project CMake files should use -standard CMake commands for exe, dll, and static library targets: - -```cmake -add_library(plugin SHARED plugin.c) -target_link_libraries(plugin PRIVATE kernel32) -``` - -The same model works with `FetchContent` without a WDK-specific wrapper: - -```cmake -include(FetchContent) - -FetchContent_Declare( - zlib - GIT_REPOSITORY https://github.com/madler/zlib.git - GIT_TAG v1.3.1 -) -FetchContent_MakeAvailable(zlib) +jobs: + build: + runs-on: windows-2025 + + steps: + - uses: actions/checkout@v4 + + - name: setup wdk7 + id: wdk7 + uses: tinysec/setup-wdk7@v1 + + - name: build with cmake + shell: cmd + run: | + cmake -S . -B build -G "${{ steps.wdk7.outputs.cmake-generator }}" ^ + -DCMAKE_TOOLCHAIN_FILE="${{ steps.wdk7.outputs.toolchain-file }}" ^ + -DWDK7_ARCH=amd64 ^ + -DCMAKE_BUILD_TYPE=Release + cmake --build build --config Release ``` -When a project needs mixed user/kernel targets, configure with -`-DWDK7_DEFAULT_MODE=NONE` and use standard CMake target commands with the -provided interface targets such as `WDK7::User`, `WDK7::Kernel`, and -`WDK7::KernelWdm`. For driver-only CMake projects, `-DWDK7_DEFAULT_MODE=KERNEL` -sets kernel compiler defaults; the `.sys` suffix, entry point, and driver linker -flags are still expressed with ordinary `set_target_properties()` and -`target_link_options()`. - -For WinDbg extensions or DbgEng programs, opt in to the Debugging Tools SDK: +For Debugging Tools and DbgEng headers/libraries: ```yaml -- name: setup wdk7 +- name: setup wdk7 with debugging tools id: wdk7 uses: tinysec/setup-wdk7@v1 with: debugger: true ``` -The action does not bundle a `FindDbgEng.cmake` module and does not define a -`WDK7::DbgEng` CMake target. Projects should keep their own find module or -target definition, typically named `DbgEng::DbgEng`, in the project repository. -When `debugger: true` is used, the action exposes the prepared SDK through -`WDK7_DBGENG_INCLUDE_DIR`, `WDK7_DBGENG_LIB_I386`, and -`WDK7_DBGENG_LIB_AMD64`. - -For legacy WDK build projects, the action also bundles `ddkbuild.cmd` and sets -`W7BASE` after WDK7 is resolved: +For legacy DDKBuild projects: ```yaml -- name: build with ddkbuild - if: steps.wdk7.outputs.found == 'true' +- name: build driver shell: cmd - run: | - call "${{ steps.wdk7.outputs.ddkbuild-cmd }}" -WIN7A64 checked src + run: call "${{ steps.wdk7.outputs.ddkbuild-cmd }}" -WIN7A64 free src ``` -## Inputs - -- `root`: explicit WDK7 root. -- `download-url`: optional WDK7 ISO URL list. Separate multiple URLs with - newlines, commas, or semicolons. These URLs are tried before the built-in - Microsoft URL. -- `debugger`: set to `true` to prepare the Debugging Tools SDK and expose DbgEng - include/library outputs. Defaults to `false`. - -## Outputs - -- `found`: `true` when WDK7 is ready. -- `root`: resolved WDK7 root. -- `source`: `input`, `environment`, `cache`, `default`, `download`, or `none`. -- `cache-hit`: `true` when an existing cached tree was reused. -- `cmake-module-dir`: absolute path to the bundled CMake module directory. -- `toolchain-file`: absolute path to the bundled CMake WDK7 toolchain file. -- `ddkbuild-cmd`: absolute path to the bundled `ddkbuild.cmd` wrapper. -- `cmake-generator`: recommended generator, currently `NMake Makefiles`. -- `dbgeng-found`: `true` when `debugger: true` and a usable DbgEng SDK was found - or prepared. -- `debuggers-root`: resolved Debugging Tools root when `dbgeng-found` is `true`. -- `dbgeng-include-dir`: DbgEng include directory when `dbgeng-found` is `true`. -- `dbgeng-lib-i386`: DbgEng x86 library directory when `dbgeng-found` is `true`. -- `dbgeng-lib-amd64`: DbgEng amd64 library directory when `dbgeng-found` is - `true`. -- `debuggers-bin-x86`: x86 Debugging Tools binary directory when available. -- `debuggers-bin-x64`: x64 Debugging Tools binary directory when available. - -## Cache Behavior - -Detection order: - -1. explicit `root` -2. `WDK7_ROOT` -3. `W7BASE` -4. default `C:\WinDDK\7600.16385.1` -5. restored/local WDK7 cache -6. download and extraction - -There is no "do not download" switch. If WDK7 is not found, the action tries the -configured download URLs and then the built-in Microsoft URL. If `debugger: -true` is set and the Debugging Tools SDK is not found, the action extracts it -from the same WDK7 ISO. To force a specific local tree, pass `root`. - -The default cache is WDK-only and uses the key `wdk-7600.16385.1`. When -`debugger: true` is used, the action uses `wdk-7600.16385.1-debugger` and may -restore the WDK-only cache first to avoid downloading WDK7 twice. - -Debugging Tools are exposed as a separate SDK surface. The action exports -`WDK7_DEBUGGERS_ROOT`, `WDK7_DBGENG_INCLUDE_DIR`, `WDK7_DBGENG_LIB_I386`, and -`WDK7_DBGENG_LIB_AMD64` only when `debugger: true`; it does not append these -directories to the generic WDK7 include/library sets. - -The default local cache root is: - -- `$RUNNER_TOOL_CACHE\wdk7` on GitHub/Gitea runners when available. -- `%LOCALAPPDATA%\actions-tool-cache\wdk7` otherwise. -- `%TEMP%\actions-tool-cache\wdk7` as a last fallback. - -On GitHub-hosted runners, the action restores and saves this directory through -the GitHub Actions cache service by default. On Gitea, cache support depends on -your runner/server configuration; if no cache service is exposed, the action -continues with the local disk cache and does not fail just because cache is -unavailable. - -When WDK7 is found from `root`, `WDK7_ROOT`, `W7BASE`, or the default -`C:\WinDDK\7600.16385.1` path, that existing local installation is used as-is -and is not saved back into the Actions cache. - -Self-hosted runner disk persistence is still the fastest path for WDK7. - -## Development - -The action is implemented in TypeScript and published as a bundled JavaScript -action: - -```powershell -cd D:\code\setup-wdk7 -npm.cmd install -npm.cmd run build -``` - -Commit both `src/main.ts` and the generated `dist/index.js`. - -PowerShell is intentionally limited to `scripts/mount-iso.ps1` and -`scripts/dismount-iso.ps1`, because `Mount-DiskImage` and `Dismount-DiskImage` -are Windows-native operations. Detection, downloads, outputs, and PATH updates -are handled by TypeScript. - -## Local Debugging - -You can debug the action without GitHub: - -```powershell -cd D:\code\setup-wdk7 -.\scripts\test-local.ps1 -``` - -If local execution policy blocks scripts, run: - -```powershell -powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\test-local.ps1 -``` - -The test script creates temporary `GITHUB_OUTPUT`, `GITHUB_ENV`, and -`GITHUB_PATH` files, runs `dist/index.js`, and prints exactly what the action -would export to later CI steps. - -The repository contains one CI workflow, `.github/workflows/ci.yaml`. It builds -the action bundle, prepares WDK7 through this action, then compiles the static -fixtures under `test/e2e`: - -- CMake plus `cmake/wdk7.cmake`: standard `add_executable()`/`add_library()` - user-mode exe, dll, static lib, a FetchContent static lib, and a Debugging - Tools-linked exe for i386 and amd64. -- CMake plus `cmake/wdk7.cmake`: standard-command WDM sys for i386 and amd64. -- `ddkbuild.cmd`: WDM sys for i386 and amd64. - -## Release - -Create the GitHub repository first, then push: - -```powershell -cd D:\code\setup-wdk7 -git init -git add . -git commit -m "Initial setup-wdk7 action" -git branch -M master -git remote add origin https://github.com/tinysec/setup-wdk7.git -git push -u origin master -git tag v1.0.0 -git tag v1 -git push origin v1.0.0 v1 -``` - -For compatible updates, create a new concrete tag and move the major tag: - -```powershell -git tag v1.0.1 -git tag -f v1 -git push origin v1.0.1 -git push -f origin v1 -``` +Use `root` to point at a preinstalled WDK7 tree, or `download-url` to provide +one or more custom ISO URLs before the built-in Microsoft URL is tried. diff --git a/cmake/wdk7.cmake b/cmake/wdk7.cmake index 8c7c42f..2c99ab2 100644 --- a/cmake/wdk7.cmake +++ b/cmake/wdk7.cmake @@ -239,16 +239,27 @@ set(_WDK7_KERNEL_LINK_OPTIONS /SECTION:INIT,d /IGNORE:4198,4010,4037,4039,4065,4070,4078,4087,4089,4221) +# Joins list values into the space-delimited flag strings expected by the WDK7 +# MSVC-compatible command line. CMake lists are easier to maintain above, while +# the compiler and linker still need plain command-line text. function(_wdk7_join out_var) + # The result is returned through PARENT_SCOPE so callers can keep all + # intermediate flag names local to the toolchain file. string(REPLACE ";" " " _joined "${ARGN}") set(${out_var} "${_joined}" PARENT_SCOPE) endfunction() +# Converts library directories to explicit /LIBPATH flags. WDK7 link behavior is +# more predictable when the exact library search order is passed to link.exe. function(_wdk7_link_directories_flags out_var) set(_flags "") + + # The explicit loop keeps each directory visible in generated cache values + # and avoids hiding path order in a compact expression. foreach (_dir IN LISTS ARGN) list(APPEND _flags "/LIBPATH:${_dir}") endforeach() + set(${out_var} "${_flags}" PARENT_SCOPE) endfunction() @@ -333,14 +344,24 @@ elseif (WDK7_DEFAULT_MODE STREQUAL "KERNEL") CACHE STRING "" FORCE) endif() +# Adds language-specific compile options to an imported interface target. This +# lets mixed C/C++ consumers inherit only the options that apply to each source +# language. function(_wdk7_interface_lang_options target lang) + # Generator expressions are kept at this boundary so project CMake files can + # stay ordinary add_executable/add_library definitions. foreach (_opt IN LISTS ARGN) set_property(TARGET "${target}" APPEND PROPERTY INTERFACE_COMPILE_OPTIONS "$<$:${_opt}>") endforeach() endfunction() +# Adds configuration-specific options for both C and C++ sources. Debug and +# release flags differ under WDK7, and consumers should inherit the correct set +# without repeating toolchain details in project files. function(_wdk7_interface_c_cxx_config_options target config) + # Both language branches are added together so Debug/Release behavior stays + # symmetric for mixed-language targets. foreach (_opt IN LISTS ARGN) set_property(TARGET "${target}" APPEND PROPERTY INTERFACE_COMPILE_OPTIONS "$<$,$>:${_opt}>" diff --git a/dist/index.js b/dist/index.js index e16818c..9142efa 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1074,14 +1074,14 @@ var require_util = __commonJS({ } const port = url2.port != null ? url2.port : url2.protocol === "https:" ? 443 : 80; let origin = url2.origin != null ? url2.origin : `${url2.protocol || ""}//${url2.hostname || ""}:${port}`; - let path13 = url2.path != null ? url2.path : `${url2.pathname || ""}${url2.search || ""}`; + let path19 = url2.path != null ? url2.path : `${url2.pathname || ""}${url2.search || ""}`; if (origin[origin.length - 1] === "/") { origin = origin.slice(0, origin.length - 1); } - if (path13 && path13[0] !== "/") { - path13 = `/${path13}`; + if (path19 && path19[0] !== "/") { + path19 = `/${path19}`; } - return new URL(`${origin}${path13}`); + return new URL(`${origin}${path19}`); } if (!isHttpOrHttpsPrefixed(url2.origin || url2.protocol)) { throw new InvalidArgumentError("Invalid URL protocol: the URL must start with `http:` or `https:`."); @@ -1532,39 +1532,39 @@ var require_diagnostics = __commonJS({ }); diagnosticsChannel.channel("undici:client:sendHeaders").subscribe((evt) => { const { - request: { method, path: path13, origin } + request: { method, path: path19, origin } } = evt; - debuglog("sending request to %s %s/%s", method, origin, path13); + debuglog("sending request to %s %s/%s", method, origin, path19); }); diagnosticsChannel.channel("undici:request:headers").subscribe((evt) => { const { - request: { method, path: path13, origin }, + request: { method, path: path19, origin }, response: { statusCode } } = evt; debuglog( "received response to %s %s/%s - HTTP %d", method, origin, - path13, + path19, statusCode ); }); diagnosticsChannel.channel("undici:request:trailers").subscribe((evt) => { const { - request: { method, path: path13, origin } + request: { method, path: path19, origin } } = evt; - debuglog("trailers received from %s %s/%s", method, origin, path13); + debuglog("trailers received from %s %s/%s", method, origin, path19); }); diagnosticsChannel.channel("undici:request:error").subscribe((evt) => { const { - request: { method, path: path13, origin }, + request: { method, path: path19, origin }, error: error2 } = evt; debuglog( "request to %s %s/%s errored - %s", method, origin, - path13, + path19, error2.message ); }); @@ -1613,9 +1613,9 @@ var require_diagnostics = __commonJS({ }); diagnosticsChannel.channel("undici:client:sendHeaders").subscribe((evt) => { const { - request: { method, path: path13, origin } + request: { method, path: path19, origin } } = evt; - debuglog("sending request to %s %s/%s", method, origin, path13); + debuglog("sending request to %s %s/%s", method, origin, path19); }); } diagnosticsChannel.channel("undici:websocket:open").subscribe((evt) => { @@ -1678,7 +1678,7 @@ var require_request = __commonJS({ var kHandler = /* @__PURE__ */ Symbol("handler"); var Request = class { constructor(origin, { - path: path13, + path: path19, method, body: body2, headers, @@ -1693,11 +1693,11 @@ var require_request = __commonJS({ expectContinue, servername }, handler) { - if (typeof path13 !== "string") { + if (typeof path19 !== "string") { throw new InvalidArgumentError("path must be a string"); - } else if (path13[0] !== "/" && !(path13.startsWith("http://") || path13.startsWith("https://")) && method !== "CONNECT") { + } else if (path19[0] !== "/" && !(path19.startsWith("http://") || path19.startsWith("https://")) && method !== "CONNECT") { throw new InvalidArgumentError("path must be an absolute URL or start with a slash"); - } else if (invalidPathRegex.test(path13)) { + } else if (invalidPathRegex.test(path19)) { throw new InvalidArgumentError("invalid request path"); } if (typeof method !== "string") { @@ -1763,7 +1763,7 @@ var require_request = __commonJS({ this.completed = false; this.aborted = false; this.upgrade = upgrade || null; - this.path = query ? buildURL(path13, query) : path13; + this.path = query ? buildURL(path19, query) : path19; this.origin = origin; this.idempotent = idempotent == null ? method === "HEAD" || method === "GET" : idempotent; this.blocking = blocking == null ? false : blocking; @@ -6327,7 +6327,7 @@ var require_client_h1 = __commonJS({ return method !== "GET" && method !== "HEAD" && method !== "OPTIONS" && method !== "TRACE" && method !== "CONNECT"; } function writeH1(client, request) { - const { method, path: path13, host, upgrade, blocking, reset } = request; + const { method, path: path19, host, upgrade, blocking, reset } = request; let { body: body2, headers, contentLength: contentLength2 } = request; const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH" || method === "QUERY" || method === "PROPFIND" || method === "PROPPATCH"; if (util5.isFormDataLike(body2)) { @@ -6393,7 +6393,7 @@ var require_client_h1 = __commonJS({ if (blocking) { socket[kBlocking] = true; } - let header = `${method} ${path13} HTTP/1.1\r + let header = `${method} ${path19} HTTP/1.1\r `; if (typeof host === "string") { header += `host: ${host}\r @@ -6919,7 +6919,7 @@ var require_client_h2 = __commonJS({ } function writeH2(client, request) { const session = client[kHTTP2Session]; - const { method, path: path13, host, upgrade, expectContinue, signal, headers: reqHeaders } = request; + const { method, path: path19, host, upgrade, expectContinue, signal, headers: reqHeaders } = request; let { body: body2 } = request; if (upgrade) { util5.errorRequest(client, request, new Error("Upgrade not supported for H2")); @@ -6986,7 +6986,7 @@ var require_client_h2 = __commonJS({ }); return true; } - headers[HTTP2_HEADER_PATH] = path13; + headers[HTTP2_HEADER_PATH] = path19; headers[HTTP2_HEADER_SCHEME] = "https"; const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH"; if (body2 && typeof body2.read === "function") { @@ -7339,9 +7339,9 @@ var require_redirect_handler = __commonJS({ return this.handler.onHeaders(statusCode, headers, resume, statusText); } const { origin, pathname, search } = util5.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))); - const path13 = search ? `${pathname}${search}` : pathname; + const path19 = search ? `${pathname}${search}` : pathname; this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin); - this.opts.path = path13; + this.opts.path = path19; this.opts.origin = origin; this.opts.maxRedirections = 0; this.opts.query = null; @@ -8576,10 +8576,10 @@ var require_proxy_agent = __commonJS({ }; const { origin, - path: path13 = "/", + path: path19 = "/", headers = {} } = opts; - opts.path = origin + path13; + opts.path = origin + path19; if (!("host" in headers) && !("Host" in headers)) { const { host } = new URL3(origin); headers.host = host; @@ -10500,20 +10500,20 @@ var require_mock_utils = __commonJS({ } return true; } - function safeUrl(path13) { - if (typeof path13 !== "string") { - return path13; + function safeUrl(path19) { + if (typeof path19 !== "string") { + return path19; } - const pathSegments = path13.split("?"); + const pathSegments = path19.split("?"); if (pathSegments.length !== 2) { - return path13; + return path19; } const qp = new URLSearchParams(pathSegments.pop()); qp.sort(); return [...pathSegments, qp.toString()].join("?"); } - function matchKey(mockDispatch2, { path: path13, method, body: body2, headers }) { - const pathMatch = matchValue(mockDispatch2.path, path13); + function matchKey(mockDispatch2, { path: path19, method, body: body2, headers }) { + const pathMatch = matchValue(mockDispatch2.path, path19); const methodMatch = matchValue(mockDispatch2.method, method); const bodyMatch = typeof mockDispatch2.body !== "undefined" ? matchValue(mockDispatch2.body, body2) : true; const headersMatch = matchHeaders(mockDispatch2, headers); @@ -10535,7 +10535,7 @@ var require_mock_utils = __commonJS({ function getMockDispatch(mockDispatches, key) { const basePath = key.query ? buildURL(key.path, key.query) : key.path; const resolvedPath = typeof basePath === "string" ? safeUrl(basePath) : basePath; - let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path: path13 }) => matchValue(safeUrl(path13), resolvedPath)); + let matchedMockDispatches = mockDispatches.filter(({ consumed }) => !consumed).filter(({ path: path19 }) => matchValue(safeUrl(path19), resolvedPath)); if (matchedMockDispatches.length === 0) { throw new MockNotMatchedError(`Mock dispatch not matched for path '${resolvedPath}'`); } @@ -10573,9 +10573,9 @@ var require_mock_utils = __commonJS({ } } function buildKey(opts) { - const { path: path13, method, body: body2, headers, query } = opts; + const { path: path19, method, body: body2, headers, query } = opts; return { - path: path13, + path: path19, method, body: body2, headers, @@ -11038,10 +11038,10 @@ var require_pending_interceptors_formatter = __commonJS({ } format(pendingInterceptors) { const withPrettyHeaders = pendingInterceptors.map( - ({ method, path: path13, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ + ({ method, path: path19, data: { statusCode }, persist, times, timesInvoked, origin }) => ({ Method: method, Origin: origin, - Path: path13, + Path: path19, "Status code": statusCode, Persistent: persist ? PERSISTENT : NOT_PERSISTENT, Invocations: timesInvoked, @@ -15922,9 +15922,9 @@ var require_util6 = __commonJS({ } } } - function validateCookiePath(path13) { - for (let i = 0; i < path13.length; ++i) { - const code = path13.charCodeAt(i); + function validateCookiePath(path19) { + for (let i = 0; i < path19.length; ++i) { + const code = path19.charCodeAt(i); if (code < 32 || // exclude CTLs (0-31) code === 127 || // DEL code === 59) { @@ -18601,11 +18601,11 @@ var require_undici = __commonJS({ if (typeof opts.path !== "string") { throw new InvalidArgumentError("invalid opts.path"); } - let path13 = opts.path; + let path19 = opts.path; if (!opts.path.startsWith("/")) { - path13 = `/${path13}`; + path19 = `/${path19}`; } - url2 = new URL(util5.parseOrigin(url2).origin + path13); + url2 = new URL(util5.parseOrigin(url2).origin + path19); } else { if (!opts) { opts = typeof url2 === "object" ? url2 : {}; @@ -18903,7 +18903,7 @@ var require_minimatch = __commonJS({ "node_modules/minimatch/minimatch.js"(exports2, module) { module.exports = minimatch2; minimatch2.Minimatch = Minimatch2; - var path13 = (function() { + var path19 = (function() { try { return __require("path"); } catch (e) { @@ -18911,7 +18911,7 @@ var require_minimatch = __commonJS({ })() || { sep: "/" }; - minimatch2.sep = path13.sep; + minimatch2.sep = path19.sep; var GLOBSTAR = minimatch2.GLOBSTAR = Minimatch2.GLOBSTAR = {}; var expand = require_brace_expansion(); var plTypes = { @@ -19000,8 +19000,8 @@ var require_minimatch = __commonJS({ assertValidPattern(pattern); if (!options) options = {}; pattern = pattern.trim(); - if (!options.allowWindowsEscape && path13.sep !== "/") { - pattern = pattern.split(path13.sep).join("/"); + if (!options.allowWindowsEscape && path19.sep !== "/") { + pattern = pattern.split(path19.sep).join("/"); } this.options = options; this.maxGlobstarRecursion = options.maxGlobstarRecursion !== void 0 ? options.maxGlobstarRecursion : 200; @@ -19372,8 +19372,8 @@ var require_minimatch = __commonJS({ if (this.empty) return f === ""; if (f === "/" && partial) return true; var options = this.options; - if (path13.sep !== "/") { - f = f.split(path13.sep).join("/"); + if (path19.sep !== "/") { + f = f.split(path19.sep).join("/"); } f = f.split(slashSplit); this.debug(this.pattern, "split", f); @@ -27139,6 +27139,10 @@ var require_commonjs2 = __commonJS({ } }); +// src/main.ts +import { mkdirSync as mkdirSync4 } from "node:fs"; +import * as path18 from "node:path"; + // node_modules/@actions/core/lib/command.js import * as os from "os"; @@ -29293,8 +29297,8 @@ var Path = class { let remaining = itemPath; let dir = dirname4(remaining); while (dir !== remaining) { - const basename6 = path6.basename(remaining); - this.segments.unshift(basename6); + const basename7 = path6.basename(remaining); + this.segments.unshift(basename7); remaining = dir; dir = dirname4(remaining); } @@ -29498,8 +29502,8 @@ var Pattern = class _Pattern { // node_modules/@actions/glob/lib/internal-search-state.js var SearchState = class { - constructor(path13, level) { - this.path = path13; + constructor(path19, level) { + this.path = path19; this.level = level; } }; @@ -34053,15 +34057,15 @@ function getRequestUrl(baseUri, operationSpec, operationArguments, fallbackObjec let isAbsolutePath = false; let requestUrl = replaceAll(baseUri, urlReplacements); if (operationSpec.path) { - let path13 = replaceAll(operationSpec.path, urlReplacements); - if (operationSpec.path === "/{nextLink}" && path13.startsWith("/")) { - path13 = path13.substring(1); + let path19 = replaceAll(operationSpec.path, urlReplacements); + if (operationSpec.path === "/{nextLink}" && path19.startsWith("/")) { + path19 = path19.substring(1); } - if (isAbsoluteUrl(path13)) { - requestUrl = path13; + if (isAbsoluteUrl(path19)) { + requestUrl = path19; isAbsolutePath = true; } else { - requestUrl = appendPath(requestUrl, path13); + requestUrl = appendPath(requestUrl, path19); } } const { queryParams, sequenceParams } = calculateQueryParameters(operationSpec, operationArguments, fallbackObject); @@ -34107,9 +34111,9 @@ function appendPath(url2, pathToAppend) { } const searchStart = pathToAppend.indexOf("?"); if (searchStart !== -1) { - const path13 = pathToAppend.substring(0, searchStart); + const path19 = pathToAppend.substring(0, searchStart); const search = pathToAppend.substring(searchStart + 1); - newPath = newPath + path13; + newPath = newPath + path19; if (search) { parsedUrl.search = parsedUrl.search ? `${parsedUrl.search}&${search}` : search; } @@ -37602,16 +37606,16 @@ var MatcherView = class { * @returns {string|undefined} */ getCurrentTag() { - const path13 = this._matcher.path; - return path13.length > 0 ? path13[path13.length - 1].tag : void 0; + const path19 = this._matcher.path; + return path19.length > 0 ? path19[path19.length - 1].tag : void 0; } /** * Get current namespace. * @returns {string|undefined} */ getCurrentNamespace() { - const path13 = this._matcher.path; - return path13.length > 0 ? path13[path13.length - 1].namespace : void 0; + const path19 = this._matcher.path; + return path19.length > 0 ? path19[path19.length - 1].namespace : void 0; } /** * Get current node's attribute value. @@ -37619,9 +37623,9 @@ var MatcherView = class { * @returns {*} */ getAttrValue(attrName) { - const path13 = this._matcher.path; - if (path13.length === 0) return void 0; - return path13[path13.length - 1].values?.[attrName]; + const path19 = this._matcher.path; + if (path19.length === 0) return void 0; + return path19[path19.length - 1].values?.[attrName]; } /** * Check if current node has an attribute. @@ -37629,9 +37633,9 @@ var MatcherView = class { * @returns {boolean} */ hasAttr(attrName) { - const path13 = this._matcher.path; - if (path13.length === 0) return false; - const current = path13[path13.length - 1]; + const path19 = this._matcher.path; + if (path19.length === 0) return false; + const current = path19[path19.length - 1]; return current.values !== void 0 && attrName in current.values; } /** @@ -37639,18 +37643,18 @@ var MatcherView = class { * @returns {number} */ getPosition() { - const path13 = this._matcher.path; - if (path13.length === 0) return -1; - return path13[path13.length - 1].position ?? 0; + const path19 = this._matcher.path; + if (path19.length === 0) return -1; + return path19[path19.length - 1].position ?? 0; } /** * Get current node's repeat counter (occurrence count of this tag name). * @returns {number} */ getCounter() { - const path13 = this._matcher.path; - if (path13.length === 0) return -1; - return path13[path13.length - 1].counter ?? 0; + const path19 = this._matcher.path; + if (path19.length === 0) return -1; + return path19[path19.length - 1].counter ?? 0; } /** * Get current node's sibling index (alias for getPosition). @@ -40040,11 +40044,11 @@ var NativeCRC64 = (() => { throw new Error("Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)"); } var scriptDirectory = ""; - function locateFile(path13) { + function locateFile(path19) { if (Module["locateFile"]) { - return Module["locateFile"](path13, scriptDirectory); + return Module["locateFile"](path19, scriptDirectory); } - return scriptDirectory + path13; + return scriptDirectory + path19; } var read_, readAsync, readBinary, setWindowTitle; function logExceptionOnExit(e) { @@ -43618,9 +43622,9 @@ var StorageSharedKeyCredentialPolicy = class extends CredentialPolicy { * @param request - */ getCanonicalizedResourceString(request) { - const path13 = getURLPath(request.url) || "/"; + const path19 = getURLPath(request.url) || "/"; let canonicalizedResourceString = ""; - canonicalizedResourceString += `/${this.factory.accountName}${path13}`; + canonicalizedResourceString += `/${this.factory.accountName}${path19}`; const queries = getURLQueries(request.url); const lowercaseQueries = {}; if (queries) { @@ -44102,9 +44106,9 @@ function storageSharedKeyCredentialPolicy(options) { return canonicalizedHeadersStringToSign; } function getCanonicalizedResourceString(request) { - const path13 = getURLPath(request.url) || "/"; + const path19 = getURLPath(request.url) || "/"; let canonicalizedResourceString = ""; - canonicalizedResourceString += `/${options.accountName}${path13}`; + canonicalizedResourceString += `/${options.accountName}${path19}`; const queries = getURLQueries(request.url); const lowercaseQueries = {}; if (queries) { @@ -58203,10 +58207,10 @@ var StorageContextClient = class extends StorageClient { // node_modules/@azure/storage-blob/dist/esm/utils/utils.common.js function escapeURLPath(url2) { const urlParsed = new URL(url2); - let path13 = urlParsed.pathname; - path13 = path13 || "/"; - path13 = escape(path13); - urlParsed.pathname = path13; + let path19 = urlParsed.pathname; + path19 = path19 || "/"; + path19 = escape(path19); + urlParsed.pathname = path19; return urlParsed.toString(); } function getProxyUriFromDevConnString(connectionString) { @@ -58291,9 +58295,9 @@ function escape(text) { } function appendToURLPath(url2, name) { const urlParsed = new URL(url2); - let path13 = urlParsed.pathname; - path13 = path13 ? path13.endsWith("/") ? `${path13}${name}` : `${path13}/${name}` : name; - urlParsed.pathname = path13; + let path19 = urlParsed.pathname; + path19 = path19 ? path19.endsWith("/") ? `${path19}${name}` : `${path19}/${name}` : name; + urlParsed.pathname = path19; return urlParsed.toString(); } function setURLParameter2(url2, name, value) { @@ -67451,14 +67455,130 @@ function saveCacheV2(paths_1, key_1, options_1) { }); } -// src/main.ts -import { createWriteStream as createWriteStream2, existsSync as existsSync4, mkdirSync, readdirSync, renameSync, rmSync, statSync as statSync2 } from "node:fs"; +// src/cache.ts +var CacheSession = class { + root; + key; + restoreKeys; + restoredKey; + restoreAttempted; + /** + * The constructor stores cache identity without touching the cache service. + * Deferring network calls keeps the main flow free to skip restore when a + * valid local WDK installation already satisfies the request. + */ + constructor(root, key, restoreKeys) { + this.root = root; + this.key = key; + this.restoreKeys = restoreKeys; + this.restoredKey = void 0; + this.restoreAttempted = false; + } + /** + * restoreOnce lazily restores the cache and returns the hit key. Repeated + * calls are common in the debugger flow, and only the first one should contact + * the cache service. + */ + async restoreOnce() { + if (false === this.restoreAttempted) { + this.restoreAttempted = true; + this.restoredKey = await restoreActionCache(this.root, this.key, this.restoreKeys); + } + return this.restoredKey; + } + /** + * hasRestored reports whether any Actions cache entry was reused. The action + * exposes this as a user-facing cache-hit signal. + */ + hasRestored() { + return void 0 !== this.restoredKey; + } + /** + * saveIfDifferentKey stores the cache when the exact requested key was not + * already restored. This also upgrades a base WDK cache into the debugger + * cache after Debugging Tools are prepared. + */ + async saveIfDifferentKey() { + if (this.key === this.restoredKey) { + return; + } + await saveActionCache(this.root, this.key); + } + /** + * saveWhenChanged is used for flows that started from a local WDK install. + * They should not save the local WDK tree, but should preserve newly extracted + * debugger files placed under the action cache root. + */ + async saveWhenChanged(changed) { + if (false === changed) { + return; + } + await this.saveIfDifferentKey(); + } +}; +async function restoreActionCache(cacheRoot, cacheKey, restoreKeys) { + if (false === isFeatureAvailable()) { + info("Actions cache service is not available; using local disk cache only."); + return void 0; + } + try { + info(`Restoring WDK7 cache with key '${cacheKey}'.`); + const hit = await restoreCache([cacheRoot], cacheKey, restoreKeys); + if (void 0 !== hit) { + info(`Restored WDK7 cache from key '${hit}'.`); + } else { + info("No WDK7 actions/cache entry was restored."); + } + return hit; + } catch (error2) { + warning(`WDK7 cache restore failed: ${formatError(error2)}`); + return void 0; + } +} +async function saveActionCache(cacheRoot, cacheKey) { + if (false === isFeatureAvailable()) { + info("Actions cache service is not available; skipping WDK7 cache save."); + return; + } + try { + info(`Saving WDK7 cache with key '${cacheKey}'.`); + await saveCache2([cacheRoot], cacheKey); + } catch (error2) { + warning(`WDK7 cache save skipped: ${formatError(error2)}`); + } +} +function formatError(error2) { + if (error2 instanceof Error) { + return error2.message; + } + return String(error2); +} + +// src/debuggers.ts +import { existsSync as existsSync7, mkdirSync as mkdirSync3 } from "node:fs"; +import * as path16 from "node:path"; + +// src/download.ts +import { createWriteStream as createWriteStream2, existsSync as existsSync4, mkdirSync, renameSync, rmSync } from "node:fs"; import * as http3 from "node:http"; import * as https3 from "node:https"; -import * as os8 from "node:os"; import * as path12 from "node:path"; -import { spawn as spawn2 } from "node:child_process"; -import { fileURLToPath as fileURLToPath2 } from "node:url"; + +// src/lists.ts +function uniqueStrings(values) { + const seen = /* @__PURE__ */ new Set(); + const result = []; + for (const value of values) { + const key = value.toLowerCase(); + if (false === seen.has(key)) { + seen.add(key); + result.push(value); + } + } + return result; +} + +// src/settings.ts var defaultDownloadUrls = [ "https://download.microsoft.com/download/4/A/2/4A25C7D5-EFBE-4182-B6A9-AE6850409A78/GRMWDK_EN_7600_1.ISO" ]; @@ -67467,456 +67587,331 @@ var wdkOnlyCacheKey = "wdk-7600.16385.1"; var debuggerCacheKey = "wdk-7600.16385.1-debugger"; var downloadRetries = 3; function readInputs() { - const downloadUrls = splitDownloadUrls(getInput("download-url")); + const configuredDownloadUrls = splitDownloadUrls(getInput("download-url")); + const downloadUrls = uniqueStrings(configuredDownloadUrls.concat(defaultDownloadUrls)); + const root = getInput("root"); + const debuggerEnabled = readBooleanInput("debugger", false); return { - root: getInput("root"), - downloadUrls: uniqueStrings([...downloadUrls, ...defaultDownloadUrls]), - debugger: readBooleanInput("debugger", false) + root, + downloadUrls, + debugger: debuggerEnabled }; } -function splitDownloadUrls(value) { - return value.split(/[\r\n,;]+/).map((item) => item.trim()).filter(Boolean); +function cacheKeyForDebugger(debuggerEnabled) { + if (true === debuggerEnabled) { + return debuggerCacheKey; + } + return wdkOnlyCacheKey; } -function uniqueStrings(values) { - const seen = /* @__PURE__ */ new Set(); +function restoreKeysForDebugger(debuggerEnabled) { + if (true === debuggerEnabled) { + return [wdkOnlyCacheKey]; + } + return []; +} +function splitDownloadUrls(value) { const result = []; - for (const value of values) { - const key = value.toLowerCase(); - if (!seen.has(key)) { - seen.add(key); - result.push(value); + const parts = value.split(/[\r\n,;]+/); + for (const part of parts) { + const trimmed = part.trim(); + if ("" !== trimmed) { + result.push(trimmed); } } return result; } function readBooleanInput(name, defaultValue) { - const value = getInput(name).trim().toLowerCase(); - if (!value) { + const rawValue = getInput(name); + const value = rawValue.trim().toLowerCase(); + if ("" === value) { return defaultValue; } - if (value === "true") { + if ("true" === value) { return true; } - if (value === "false") { + if ("false" === value) { return false; } throw new Error(`Input '${name}' must be true or false.`); } + +// src/download.ts +var maxRedirects = 8; +var requestTimeoutMilliseconds = 3e5; +async function ensureWdk7Iso(cacheRoot, urls) { + const isoPath = path12.join(cacheRoot, "GRMWDK_EN_7600_1.ISO"); + if (true === existsSync4(isoPath)) { + info(`Using cached WDK7 ISO: ${isoPath}`); + return isoPath; + } + const downloadedUrl = await downloadFileFromUrlsWithRetries(urls, isoPath, downloadRetries); + info(`Downloaded WDK7 ISO from: ${downloadedUrl}`); + return isoPath; +} +async function downloadFileFromUrlsWithRetries(urls, outputPath, attempts) { + let lastError = void 0; + for (let index = 0; index < urls.length; index = index + 1) { + const url2 = urls[index]; + try { + info(`Downloading WDK7 ISO from source ${index + 1}/${urls.length}: ${url2}`); + await downloadFileWithRetries(url2, outputPath, attempts); + return url2; + } catch (error2) { + lastError = error2; + rmSync(outputPath, { force: true }); + if (index + 1 < urls.length) { + warning(`WDK7 ISO source ${index + 1}/${urls.length} failed: ${formatError2(error2)}. Trying next source.`); + } + } + } + throw errorFromUnknown(lastError); +} +async function downloadFileWithRetries(urlText, outputPath, attempts) { + let lastError = void 0; + for (let attempt = 1; attempt <= attempts; attempt = attempt + 1) { + try { + await downloadFile(urlText, outputPath, 0); + return; + } catch (error2) { + lastError = error2; + rmSync(outputPath, { force: true }); + if (attempt >= attempts) { + break; + } + const delay4 = Math.min(3e4, 2e3 * attempt); + warning(`WDK7 ISO download attempt ${attempt}/${attempts} failed: ${formatError2(error2)}. Retrying in ${delay4 / 1e3}s.`); + await sleep2(delay4); + } + } + throw errorFromUnknown(lastError); +} +async function downloadFile(urlText, outputPath, redirectCount) { + if (maxRedirects < redirectCount) { + throw new Error(`Too many redirects while downloading '${urlText}'.`); + } + mkdirSync(path12.dirname(outputPath), { recursive: true }); + const url2 = new URL(urlText); + const tmpPath = `${outputPath}.tmp`; + rmSync(tmpPath, { force: true }); + function downloadFilePromise(resolve3, reject) { + function onResponse(response) { + let status = 0; + if (void 0 !== response.statusCode) { + status = response.statusCode; + } + const location = response.headers.location; + if (300 <= status && 400 > status && void 0 !== location) { + response.resume(); + const nextUrl = new URL(location, url2).toString(); + downloadFile(nextUrl, outputPath, redirectCount + 1).then(resolve3, reject); + return; + } + if (200 > status || 300 <= status) { + response.resume(); + reject(new Error(`Download failed with HTTP ${status}: ${urlText}`)); + return; + } + writeResponseToFile(response, tmpPath, outputPath, resolve3, reject); + } + const client = "https:" === url2.protocol ? https3 : http3; + const request = client.get(url2, onResponse); + request.on("error", function onRequestError(error2) { + rmSync(tmpPath, { force: true }); + reject(error2); + }); + request.setTimeout(requestTimeoutMilliseconds, function onRequestTimeout() { + request.destroy(new Error(`Download timed out after 300 seconds: ${urlText}`)); + }); + } + await new Promise(downloadFilePromise); +} +function writeResponseToFile(response, tmpPath, outputPath, resolve3, reject) { + const file = createWriteStream2(tmpPath); + response.pipe(file); + file.on("finish", function onFileFinish() { + function onFileClosed() { + renameSync(tmpPath, outputPath); + resolve3(); + } + file.close(onFileClosed); + }); + file.on("error", function onFileError(error2) { + rmSync(tmpPath, { force: true }); + reject(error2); + }); +} +function sleep2(milliseconds) { + function sleepPromise(resolve3) { + setTimeout(resolve3, milliseconds); + } + return new Promise(sleepPromise); +} +function formatError2(error2) { + if (error2 instanceof Error) { + return error2.message; + } + return String(error2); +} +function errorFromUnknown(error2) { + if (error2 instanceof Error) { + return error2; + } + return new Error(String(error2)); +} + +// src/files.ts +import { existsSync as existsSync5, readdirSync, statSync as statSync2 } from "node:fs"; +import * as path14 from "node:path"; + +// src/paths.ts +import * as os8 from "node:os"; +import * as path13 from "node:path"; +import { fileURLToPath as fileURLToPath2 } from "node:url"; function actionRoot() { - return path12.dirname(path12.dirname(fileURLToPath2(import.meta.url))); + const bundledEntry = fileURLToPath2(import.meta.url); + const distDirectory = path13.dirname(bundledEntry); + return path13.dirname(distDirectory); } function cmakeModuleDir() { - return path12.join(actionRoot(), "cmake"); + return path13.join(actionRoot(), "cmake"); } function toolchainFile() { - return path12.join(cmakeModuleDir(), "wdk7.cmake"); + return path13.join(cmakeModuleDir(), "wdk7.cmake"); } function ddkbuildCmd() { - return path12.join(actionRoot(), "ddkbuild.cmd"); -} -function publishStaticOutputs() { - setOutput("cmake-module-dir", cmakeModuleDir()); - setOutput("toolchain-file", toolchainFile()); - setOutput("ddkbuild-cmd", ddkbuildCmd()); - setOutput("cmake-generator", cmakeGenerator); + return path13.join(actionRoot(), "ddkbuild.cmd"); } function expandEnvironment(value) { - return value.replace(/%([^%]+)%/g, (_match, name) => process.env[name] ?? ""); + return value.replace(/%([^%]+)%/g, replaceEnvironmentToken); } function fullPath(value) { - if (!value.trim()) { + if ("" === value.trim()) { return ""; } - return path12.resolve(expandEnvironment(value)); + return path13.resolve(expandEnvironment(value)); } function targetBins(root) { return [ - path12.join(root, "bin", "x86", "x86"), - path12.join(root, "bin", "x86", "amd64") + path13.join(root, "bin", "x86", "x86"), + path13.join(root, "bin", "x86", "amd64") ]; } function hostBin(root) { - return path12.join(root, "bin", "x86"); -} -function isFileOrDirectoryPresent(candidatePath) { - return existsSync4(candidatePath); -} -function isWdk7Root(root) { - if (!root.trim()) { - return false; - } - const resolved = fullPath(root); - const required = [ - path12.join(resolved, "bin", "setenv.bat"), - path12.join(resolved, "inc", "api"), - path12.join(resolved, "inc", "ddk"), - path12.join(hostBin(resolved), "nmake.exe"), - path12.join(hostBin(resolved), "rc.exe"), - ...targetBins(resolved).flatMap((bin) => [ - path12.join(bin, "cl.exe"), - path12.join(bin, "link.exe") - ]) - ]; - return required.every(isFileOrDirectoryPresent); + return path13.join(root, "bin", "x86"); } function defaultCacheRoot() { - if (process.env.RUNNER_TOOL_CACHE) { - return path12.join(process.env.RUNNER_TOOL_CACHE, "wdk7"); - } - if (process.env.LOCALAPPDATA) { - return path12.join(process.env.LOCALAPPDATA, "actions-tool-cache", "wdk7"); + const runnerToolCache = process.env.RUNNER_TOOL_CACHE; + const localAppData = process.env.LOCALAPPDATA; + if (void 0 !== runnerToolCache && "" !== runnerToolCache) { + return path13.join(runnerToolCache, "wdk7"); } - return path12.join(os8.tmpdir(), "actions-tool-cache", "wdk7"); -} -function addCandidate(candidates, root, source) { - if (!root?.trim()) { - return; - } - const resolved = fullPath(root); - if (!candidates.some((candidate) => candidate.root.toLowerCase() === resolved.toLowerCase())) { - candidates.push({ root: resolved, source }); + if (void 0 !== localAppData && "" !== localAppData) { + return path13.join(localAppData, "actions-tool-cache", "wdk7"); } + return path13.join(os8.tmpdir(), "actions-tool-cache", "wdk7"); } -function findWdk7Root(requestedRoot, cacheRoot, includeCache) { - const candidates = []; - addCandidate(candidates, requestedRoot, "input"); - addCandidate(candidates, process.env.WDK7_ROOT, "environment"); - addCandidate(candidates, process.env.W7BASE, "environment"); - if (includeCache) { - addCandidate(candidates, path12.join(cacheRoot, "7600.16385.1"), "cache"); - addCandidate(candidates, path12.join(cacheRoot, "7600.16385.win7_wdk.100208-1538"), "cache"); - addCandidate(candidates, path12.join(cacheRoot, "wdk7", "7600.16385.1"), "cache"); - addCandidate(candidates, path12.join(cacheRoot, "wdk7", "7600.16385.win7_wdk.100208-1538"), "cache"); - } - addCandidate(candidates, "C:\\WinDDK\\7600.16385.1", "default"); - addCandidate(candidates, "C:\\WinDDK\\7600.16385.win7_wdk.100208-1538", "default"); - return candidates.find((candidate) => isWdk7Root(candidate.root)); -} -function findWdk7RootUnder(basePath) { - const resolvedBase = fullPath(basePath); - if (!resolvedBase || !existsSync4(resolvedBase)) { - return void 0; - } - if (isWdk7Root(resolvedBase)) { - return resolvedBase; - } - const stack = [resolvedBase]; - while (stack.length > 0) { - const current = stack.pop(); - if (!current) { - continue; - } - let entries; - try { - entries = readdirSync(current); - } catch { - continue; - } - for (const entry of entries) { - const entryPath = path12.join(current, entry); - let stats; - try { - stats = statSync2(entryPath); - } catch { - continue; - } - if (stats.isDirectory()) { - stack.push(entryPath); - } else if (entry.toLowerCase() === "setenv.bat") { - const root = path12.dirname(path12.dirname(entryPath)); - if (isWdk7Root(root)) { - return fullPath(root); - } - } - } - } - return void 0; -} -function findCachedWdk7Root(cacheRoot) { - const root = findWdk7RootUnder(cacheRoot); - return root ? { root, source: "cache" } : void 0; -} -function hasDbgEngInclude(includeDir) { - return existsSync4(path12.join(includeDir, "DbgEng.h")); -} -function hasDbgEngLibraries(libraryDir) { - return existsSync4(path12.join(libraryDir, "dbgeng.lib")) && existsSync4(path12.join(libraryDir, "dbghelp.lib")); -} -function debuggerBin(root, arch2) { - const candidates = [ - path12.join(root, arch2), - path12.join(root, "Debuggers", arch2), - path12.join(root, "Debugging Tools for Windows", arch2), - path12.join(root, `Debugging Tools for Windows (${arch2})`) - ]; - return candidates.find((candidate) => existsSync4(candidate)) ?? ""; -} -function createDebuggersSdk(root, includeDir, libI386, libAmd64) { - if (!hasDbgEngInclude(includeDir) || !hasDbgEngLibraries(libI386) || !hasDbgEngLibraries(libAmd64)) { - return void 0; - } - const resolvedRoot = fullPath(root); - return { - root: resolvedRoot, - includeDir: fullPath(includeDir), - libI386: fullPath(libI386), - libAmd64: fullPath(libAmd64), - binX86: debuggerBin(resolvedRoot, "x86"), - binX64: debuggerBin(resolvedRoot, "x64") - }; -} -function findDbgEngSdkInKnownLayouts(root) { - const resolved = fullPath(root); - if (!resolved || !existsSync4(resolved)) { - return void 0; - } - const layouts = [ - { - root: path12.join(resolved, "Debuggers"), - includeDir: path12.join(resolved, "Debuggers", "sdk", "inc"), - libI386: path12.join(resolved, "Debuggers", "sdk", "lib", "i386"), - libAmd64: path12.join(resolved, "Debuggers", "sdk", "lib", "amd64") - }, - { - root: resolved, - includeDir: path12.join(resolved, "sdk", "inc"), - libI386: path12.join(resolved, "sdk", "lib", "i386"), - libAmd64: path12.join(resolved, "sdk", "lib", "amd64") - }, - { - root: path12.join(resolved, "Debuggers"), - includeDir: path12.join(resolved, "Debuggers", "inc"), - libI386: path12.join(resolved, "Debuggers", "lib", "x86"), - libAmd64: path12.join(resolved, "Debuggers", "lib", "x64") - }, - { - root: resolved, - includeDir: path12.join(resolved, "inc"), - libI386: path12.join(resolved, "lib", "x86"), - libAmd64: path12.join(resolved, "lib", "x64") - }, - { - root: resolved, - includeDir: path12.join(resolved, "Include"), - libI386: path12.join(resolved, "Lib"), - libAmd64: path12.join(resolved, "Lib", "x64") - } - ]; - for (const layout of layouts) { - const sdk = createDebuggersSdk(layout.root, layout.includeDir, layout.libI386, layout.libAmd64); - if (sdk) { - return sdk; - } +function replaceEnvironmentToken(_match, name) { + const replacement = process.env[name]; + if (void 0 === replacement) { + return ""; } - return void 0; + return replacement; } + +// src/files.ts function listFilesUnder(root, predicate, maxDepth = 10) { - const resolved = fullPath(root); - if (!resolved || !existsSync4(resolved)) { + const resolvedRoot = fullPath(root); + if ("" === resolvedRoot || false === existsSync5(resolvedRoot)) { return []; } const result = []; - const stack = [{ dir: resolved, depth: 0 }]; - while (stack.length > 0) { + const stack = [{ dir: resolvedRoot, depth: 0 }]; + while (0 < stack.length) { const current = stack.pop(); - if (!current) { - continue; - } - let entries; - try { - entries = readdirSync(current.dir); - } catch { + if (void 0 === current) { continue; } + const entries = readDirectoryEntries(current.dir); for (const entry of entries) { - const entryPath = path12.join(current.dir, entry); - let stats; - try { - stats = statSync2(entryPath); - } catch { + const entryPath = path14.join(current.dir, entry); + const stats = readStats(entryPath); + if (void 0 === stats) { continue; } - if (stats.isDirectory()) { + if (true === stats.isDirectory()) { if (current.depth < maxDepth) { stack.push({ dir: entryPath, depth: current.depth + 1 }); } - } else if (predicate(entryPath)) { + } else if (true === predicate(entryPath)) { result.push(entryPath); } } } return result; } -function findDbgEngSdkUnder(root) { - const known = findDbgEngSdkInKnownLayouts(root); - if (known) { - return known; - } - const includeFiles = listFilesUnder(root, (filePath) => path12.basename(filePath).toLowerCase() === "dbgeng.h"); - if (includeFiles.length === 0) { - return void 0; - } - const libDirs = uniqueStrings( - listFilesUnder(root, (filePath) => path12.basename(filePath).toLowerCase() === "dbgeng.lib").map((filePath) => path12.dirname(filePath)).filter(hasDbgEngLibraries) - ); - if (libDirs.length === 0) { - return void 0; +function readDirectoryEntries(directory) { + try { + return readdirSync(directory); + } catch { + return []; } - const amd64Lib = libDirs.find((dir) => /[\\\/](amd64|x64)([\\\/]|$)/i.test(dir)); - const i386Lib = libDirs.find((dir) => /[\\\/](i386|x86)([\\\/]|$)/i.test(dir) && !/[\\\/](amd64|x64)([\\\/]|$)/i.test(dir)) ?? libDirs.find((dir) => !/[\\\/](amd64|x64)([\\\/]|$)/i.test(dir)); - if (!i386Lib || !amd64Lib) { +} +function readStats(filePath) { + try { + return statSync2(filePath); + } catch { return void 0; } - for (const includeFile of includeFiles) { - const includeDir = path12.dirname(includeFile); - let debuggerRoot = path12.dirname(includeDir); - if (path12.basename(debuggerRoot).toLowerCase() === "sdk") { - debuggerRoot = path12.dirname(debuggerRoot); - } - const sdk = createDebuggersSdk(debuggerRoot, includeDir, i386Lib, amd64Lib); - if (sdk) { - return sdk; - } - } - return void 0; -} -function findDbgEngSdk(wdkRoot, cacheRoot) { - return findDbgEngSdkUnder(wdkRoot) ?? findDbgEngSdkUnder(cacheRoot); } + +// src/iso.ts +import { existsSync as existsSync6, mkdirSync as mkdirSync2, readdirSync as readdirSync2 } from "node:fs"; +import * as path15 from "node:path"; + +// src/process.ts +import { spawn as spawn2 } from "node:child_process"; function runProcess(command, args, options) { - return new Promise((resolve3, reject) => { + function runProcessPromise(resolve3, reject) { + const cwd = void 0 !== options ? options.cwd : void 0; + const silent = void 0 !== options && true === options.silent; debug(`Running: ${command} ${args.join(" ")}`); const child2 = spawn2(command, args, { - cwd: options?.cwd, + cwd, windowsHide: true }); let stdout = ""; let stderr = ""; - child2.stdout.on("data", (chunk) => { + child2.stdout.on("data", function onStdoutData(chunk) { const text = chunk.toString(); - stdout += text; - if (!options?.silent) { + stdout = stdout + text; + if (false === silent) { process.stdout.write(text); } }); - child2.stderr.on("data", (chunk) => { + child2.stderr.on("data", function onStderrData(chunk) { const text = chunk.toString(); - stderr += text; - if (!options?.silent) { + stderr = stderr + text; + if (false === silent) { process.stderr.write(text); } }); - child2.on("error", reject); - child2.on("close", (code) => { - if (code === 0) { - resolve3(stdout.trim()); - } else { - reject(new Error(`${command} failed with exit code ${code}. ${stderr.trim()}`)); - } + child2.on("error", function onChildError(error2) { + reject(error2); }); - }); -} -function sleep2(milliseconds) { - return new Promise((resolve3) => setTimeout(resolve3, milliseconds)); -} -async function downloadFile(urlText, outputPath, redirectCount = 0) { - if (redirectCount > 8) { - throw new Error(`Too many redirects while downloading '${urlText}'.`); - } - mkdirSync(path12.dirname(outputPath), { recursive: true }); - const url2 = new URL(urlText); - const client = url2.protocol === "https:" ? https3 : http3; - const tmpPath = `${outputPath}.tmp`; - await new Promise((resolve3, reject) => { - const request = client.get(url2, (response) => { - const status = response.statusCode ?? 0; - const location = response.headers.location; - if (status >= 300 && status < 400 && location) { - response.resume(); - const nextUrl = new URL(location, url2).toString(); - downloadFile(nextUrl, outputPath, redirectCount + 1).then(resolve3, reject); - return; - } - if (status < 200 || status >= 300) { - response.resume(); - reject(new Error(`Download failed with HTTP ${status}: ${urlText}`)); + child2.on("close", function onChildClose(code) { + if (0 === code) { + resolve3(stdout.trim()); return; } - const file = createWriteStream2(tmpPath); - response.pipe(file); - file.on("finish", () => { - file.close(() => { - renameSync(tmpPath, outputPath); - resolve3(); - }); - }); - file.on("error", (error2) => { - rmSync(tmpPath, { force: true }); - reject(error2); - }); - }); - request.on("error", (error2) => { - rmSync(tmpPath, { force: true }); - reject(error2); - }); - request.setTimeout(3e5, () => { - request.destroy(new Error(`Download timed out after 300 seconds: ${urlText}`)); + reject(new Error(`${command} failed with exit code ${code}. ${stderr.trim()}`)); }); - }); -} -async function downloadFileWithRetries(urlText, outputPath, attempts) { - let lastError; - for (let attempt = 1; attempt <= attempts; attempt += 1) { - try { - await downloadFile(urlText, outputPath); - return; - } catch (error2) { - lastError = error2; - rmSync(outputPath, { force: true }); - if (attempt >= attempts) { - break; - } - const delay4 = Math.min(3e4, 2e3 * attempt); - warning( - `WDK7 ISO download attempt ${attempt}/${attempts} failed: ${error2 instanceof Error ? error2.message : String(error2)}. Retrying in ${delay4 / 1e3}s.` - ); - await sleep2(delay4); - } } - throw lastError instanceof Error ? lastError : new Error(String(lastError)); -} -async function downloadFileFromUrlsWithRetries(urls, outputPath, attempts) { - let lastError; - for (let index = 0; index < urls.length; index += 1) { - const url2 = urls[index]; - try { - info(`Downloading WDK7 ISO from source ${index + 1}/${urls.length}: ${url2}`); - await downloadFileWithRetries(url2, outputPath, attempts); - return url2; - } catch (error2) { - lastError = error2; - rmSync(outputPath, { force: true }); - if (index + 1 < urls.length) { - warning( - `WDK7 ISO source ${index + 1}/${urls.length} failed: ${error2 instanceof Error ? error2.message : String(error2)}. Trying next source.` - ); - } - } - } - throw lastError instanceof Error ? lastError : new Error(String(lastError)); -} -async function ensureWdk7Iso(cacheRoot, urls) { - const isoPath = path12.join(cacheRoot, "GRMWDK_EN_7600_1.ISO"); - if (existsSync4(isoPath)) { - info(`Using cached WDK7 ISO: ${isoPath}`); - return isoPath; - } - const downloadedUrl = await downloadFileFromUrlsWithRetries(urls, isoPath, downloadRetries); - info(`Downloaded WDK7 ISO from: ${downloadedUrl}`); - return isoPath; + return new Promise(runProcessPromise); } + +// src/iso.ts async function mountIso(isoPath) { - const script = path12.join(actionRoot(), "scripts", "mount-iso.ps1"); + const script = path15.join(actionRoot(), "scripts", "mount-iso.ps1"); const output = await runProcess("powershell.exe", [ "-NoProfile", "-ExecutionPolicy", @@ -67926,14 +67921,14 @@ async function mountIso(isoPath) { "-ImagePath", isoPath ], { silent: true }); - const drive = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop(); - if (!drive) { + const drive = lastNonEmptyLine(output); + if ("" === drive) { throw new Error("Mount-DiskImage did not return a drive letter."); } return drive.replace(":", ""); } async function dismountIso(isoPath) { - const script = path12.join(actionRoot(), "scripts", "dismount-iso.ps1"); + const script = path15.join(actionRoot(), "scripts", "dismount-iso.ps1"); await runProcess("powershell.exe", [ "-NoProfile", "-ExecutionPolicy", @@ -67944,56 +67939,233 @@ async function dismountIso(isoPath) { isoPath ], { silent: true }); } -function findDebuggersMsiFiles(mediaRoot) { - return listFilesUnder(mediaRoot, (filePath) => { - const lower = filePath.toLowerCase(); - const baseName = path12.basename(lower); - return baseName.endsWith(".msi") && (baseName.startsWith("dbg") || lower.includes("debuggingtools") || lower.includes("debuggers")); - }); -} async function extractMsi(msiPath, targetRoot, logRoot) { - const baseName = path12.basename(msiPath, path12.extname(msiPath)); - const logPath = path12.join(logRoot, `${baseName}.log`); + const baseName = path15.basename(msiPath, path15.extname(msiPath)); + const logPath = path15.join(logRoot, `${baseName}.log`); try { - info(`Extracting ${path12.basename(msiPath)} to ${targetRoot}`); - await runProcess("msiexec.exe", [ - "/a", - msiPath, - "/qn", - "/norestart", - `TARGETDIR=${targetRoot}`, - "/l*v", - logPath - ]); + info(`Extracting ${path15.basename(msiPath)} to ${targetRoot}`); + await runMsiExtraction(msiPath, targetRoot, logPath); return true; } catch (error2) { - info(`Skipping ${path12.basename(msiPath)}: ${error2 instanceof Error ? error2.message : String(error2)}`); + info(`Skipping ${path15.basename(msiPath)}: ${formatError3(error2)}`); return false; } } +async function installWdk7FromIso(isoPath, targetRoot) { + info(`Mounting WDK7 ISO: ${isoPath}`); + const drive = await mountIso(isoPath); + try { + const mediaRoot = `${drive}:\\WDK`; + if (false === existsSync6(mediaRoot)) { + throw new Error(`Mounted ISO does not contain a WDK directory: ${mediaRoot}`); + } + mkdirSync2(targetRoot, { recursive: true }); + const logRoot = path15.join(targetRoot, "_install_logs"); + mkdirSync2(logRoot, { recursive: true }); + const msiFiles = listWdkMsiFiles(mediaRoot); + if (0 === msiFiles.length) { + throw new Error(`No WDK MSI packages found under '${mediaRoot}'.`); + } + for (const msiPath of msiFiles) { + const baseName = path15.basename(msiPath, path15.extname(msiPath)); + const logPath = path15.join(logRoot, `${baseName}.log`); + info(`Extracting ${path15.basename(msiPath)}`); + await runMsiExtraction(msiPath, targetRoot, logPath); + } + } finally { + await dismountIso(isoPath); + } +} +async function runMsiExtraction(msiPath, targetRoot, logPath) { + await runProcess("msiexec.exe", [ + "/a", + msiPath, + "/qn", + "/norestart", + `TARGETDIR=${targetRoot}`, + "/l*v", + logPath + ]); +} +function listWdkMsiFiles(mediaRoot) { + const result = []; + const entries = readdirSync2(mediaRoot); + for (const entry of entries) { + if (true === entry.toLowerCase().endsWith(".msi")) { + result.push(path15.join(mediaRoot, entry)); + } + } + return result; +} +function lastNonEmptyLine(output) { + let result = ""; + const lines = output.split(/\r?\n/); + for (const line of lines) { + const trimmed = line.trim(); + if ("" !== trimmed) { + result = trimmed; + } + } + return result; +} +function formatError3(error2) { + if (error2 instanceof Error) { + return error2.message; + } + return String(error2); +} + +// src/debuggers.ts +function findDbgEngSdk(wdkRoot, cacheRoot) { + const sdkFromWdk = findDbgEngSdkUnder(wdkRoot); + if (void 0 !== sdkFromWdk) { + return sdkFromWdk; + } + return findDbgEngSdkUnder(cacheRoot); +} +async function prepareOptionalDebuggersSdk(enabled2, wdkRoot, cacheRoot, downloadUrls) { + if (false === enabled2) { + info("Debugging Tools SDK was not requested. Set debugger: true to prepare DbgEng headers and libraries."); + return { cacheChanged: false }; + } + return prepareDebuggersSdk(wdkRoot, cacheRoot, downloadUrls); +} +async function prepareDebuggersSdk(wdkRoot, cacheRoot, downloadUrls) { + let sdk = findDbgEngSdk(wdkRoot, cacheRoot); + if (void 0 !== sdk) { + return { + sdk, + cacheChanged: false + }; + } + if (0 === downloadUrls.length) { + info("WDK7 Debuggers SDK was not found and no download URLs are configured."); + return { cacheChanged: false }; + } + const isoPath = await ensureWdk7Iso(cacheRoot, downloadUrls); + const changed = await installDebuggersFromIso(isoPath, wdkRoot, cacheRoot); + sdk = findDbgEngSdk(wdkRoot, cacheRoot); + if (void 0 === sdk) { + info("WDK7 Debuggers SDK was not found after Debugging Tools extraction."); + } + return { + sdk, + cacheChanged: changed + }; +} +function hasDbgEngInclude(includeDir) { + return existsSync7(path16.join(includeDir, "DbgEng.h")); +} +function hasDbgEngLibraries(libraryDir) { + return existsSync7(path16.join(libraryDir, "dbgeng.lib")) && existsSync7(path16.join(libraryDir, "dbghelp.lib")); +} +function debuggerBin(root, arch2) { + const candidates = [ + path16.join(root, arch2), + path16.join(root, "Debuggers", arch2), + path16.join(root, "Debugging Tools for Windows", arch2), + path16.join(root, `Debugging Tools for Windows (${arch2})`) + ]; + for (const candidate of candidates) { + if (true === existsSync7(candidate)) { + return candidate; + } + } + return ""; +} +function createDebuggersSdk(root, includeDir, libI386, libAmd64) { + if (false === hasDbgEngInclude(includeDir)) { + return void 0; + } + if (false === hasDbgEngLibraries(libI386)) { + return void 0; + } + if (false === hasDbgEngLibraries(libAmd64)) { + return void 0; + } + const resolvedRoot = fullPath(root); + return { + root: resolvedRoot, + includeDir: fullPath(includeDir), + libI386: fullPath(libI386), + libAmd64: fullPath(libAmd64), + binX86: debuggerBin(resolvedRoot, "x86"), + binX64: debuggerBin(resolvedRoot, "x64") + }; +} +function findDbgEngSdkInKnownLayouts(root) { + const resolved = fullPath(root); + if ("" === resolved || false === existsSync7(resolved)) { + return void 0; + } + const layouts = knownLayouts(resolved); + for (const layout of layouts) { + const sdk = createDebuggersSdk( + layout.root, + layout.includeDir, + layout.libI386, + layout.libAmd64 + ); + if (void 0 !== sdk) { + return sdk; + } + } + return void 0; +} +function findDbgEngSdkUnder(root) { + const known = findDbgEngSdkInKnownLayouts(root); + if (void 0 !== known) { + return known; + } + const includeFiles = findDbgEngHeaders(root); + if (0 === includeFiles.length) { + return void 0; + } + const libDirs = findDbgEngLibraryDirs(root); + if (0 === libDirs.length) { + return void 0; + } + const amd64Lib = findAmd64Library(libDirs); + const i386Lib = findI386Library(libDirs); + if (void 0 === i386Lib || void 0 === amd64Lib) { + return void 0; + } + for (const includeFile of includeFiles) { + const includeDir = path16.dirname(includeFile); + const debuggerRoot = debuggerRootFromIncludeDir(includeDir); + const sdk = createDebuggersSdk(debuggerRoot, includeDir, i386Lib, amd64Lib); + if (void 0 !== sdk) { + return sdk; + } + } + return void 0; +} async function installDebuggersFromIso(isoPath, wdkRoot, cacheRoot) { info(`Mounting WDK7 ISO for Debugging Tools: ${isoPath}`); const drive = await mountIso(isoPath); try { const mediaRoot = `${drive}:\\`; const msiFiles = findDebuggersMsiFiles(mediaRoot); - if (msiFiles.length === 0) { + if (0 === msiFiles.length) { info("No Debugging Tools MSI packages were found in the WDK7 ISO."); return false; } let changed = false; const targetRoots = uniqueStrings([ wdkRoot, - path12.join(cacheRoot, "debuggers") + path16.join(cacheRoot, "debuggers") ]); for (const targetRoot of targetRoots) { - mkdirSync(targetRoot, { recursive: true }); - const logRoot = path12.join(targetRoot, "_debuggers_install_logs"); - mkdirSync(logRoot, { recursive: true }); + mkdirSync3(targetRoot, { recursive: true }); + const logRoot = path16.join(targetRoot, "_debuggers_install_logs"); + mkdirSync3(logRoot, { recursive: true }); for (const msiPath of msiFiles) { - changed = await extractMsi(msiPath, targetRoot, logRoot) || changed; + const extracted = await extractMsi(msiPath, targetRoot, logRoot); + if (true === extracted) { + changed = true; + } } - if (findDbgEngSdk(wdkRoot, cacheRoot)) { + if (void 0 !== findDbgEngSdk(wdkRoot, cacheRoot)) { return changed; } } @@ -68002,89 +68174,148 @@ async function installDebuggersFromIso(isoPath, wdkRoot, cacheRoot) { await dismountIso(isoPath); } } -async function prepareDebuggersSdk(wdkRoot, cacheRoot, downloadUrls) { - let sdk = findDbgEngSdk(wdkRoot, cacheRoot); - if (sdk) { - return { sdk, cacheChanged: false }; - } - if (downloadUrls.length === 0) { - info("WDK7 Debuggers SDK was not found and no download URLs are configured."); - return { cacheChanged: false }; - } - const isoPath = await ensureWdk7Iso(cacheRoot, downloadUrls); - const changed = await installDebuggersFromIso(isoPath, wdkRoot, cacheRoot); - sdk = findDbgEngSdk(wdkRoot, cacheRoot); - if (!sdk) { - info("WDK7 Debuggers SDK was not found after Debugging Tools extraction."); - } - return { sdk, cacheChanged: changed }; -} -async function installWdk7FromIso(isoPath, targetRoot) { - info(`Mounting WDK7 ISO: ${isoPath}`); - const drive = await mountIso(isoPath); - try { - const mediaRoot = `${drive}:\\WDK`; - if (!existsSync4(mediaRoot)) { - throw new Error(`Mounted ISO does not contain a WDK directory: ${mediaRoot}`); +function knownLayouts(resolved) { + return [ + { + root: path16.join(resolved, "Debuggers"), + includeDir: path16.join(resolved, "Debuggers", "sdk", "inc"), + libI386: path16.join(resolved, "Debuggers", "sdk", "lib", "i386"), + libAmd64: path16.join(resolved, "Debuggers", "sdk", "lib", "amd64") + }, + { + root: resolved, + includeDir: path16.join(resolved, "sdk", "inc"), + libI386: path16.join(resolved, "sdk", "lib", "i386"), + libAmd64: path16.join(resolved, "sdk", "lib", "amd64") + }, + { + root: path16.join(resolved, "Debuggers"), + includeDir: path16.join(resolved, "Debuggers", "inc"), + libI386: path16.join(resolved, "Debuggers", "lib", "x86"), + libAmd64: path16.join(resolved, "Debuggers", "lib", "x64") + }, + { + root: resolved, + includeDir: path16.join(resolved, "inc"), + libI386: path16.join(resolved, "lib", "x86"), + libAmd64: path16.join(resolved, "lib", "x64") + }, + { + root: resolved, + includeDir: path16.join(resolved, "Include"), + libI386: path16.join(resolved, "Lib"), + libAmd64: path16.join(resolved, "Lib", "x64") } - mkdirSync(targetRoot, { recursive: true }); - const logRoot = path12.join(targetRoot, "_install_logs"); - mkdirSync(logRoot, { recursive: true }); - const msiFiles = readdirSync(mediaRoot).filter((entry) => entry.toLowerCase().endsWith(".msi")).map((entry) => path12.join(mediaRoot, entry)); - if (msiFiles.length === 0) { - throw new Error(`No WDK MSI packages found under '${mediaRoot}'.`); + ]; +} +function findDbgEngHeaders(root) { + return listFilesUnder(root, isDbgEngHeader); +} +function findDbgEngLibraryDirs(root) { + const files = listFilesUnder(root, isDbgEngLibrary); + const dirs = []; + for (const filePath of files) { + const dir = path16.dirname(filePath); + if (true === hasDbgEngLibraries(dir)) { + dirs.push(dir); } - for (const msiPath of msiFiles) { - const baseName = path12.basename(msiPath, path12.extname(msiPath)); - const logPath = path12.join(logRoot, `${baseName}.log`); - info(`Extracting ${path12.basename(msiPath)}`); - await runProcess("msiexec.exe", [ - "/a", - msiPath, - "/qn", - "/norestart", - `TARGETDIR=${targetRoot}`, - "/l*v", - logPath - ]); + } + return uniqueStrings(dirs); +} +function findAmd64Library(libDirs) { + for (const dir of libDirs) { + if (true === containsArchSegment(dir, ["amd64", "x64"])) { + return dir; } - } finally { - await dismountIso(isoPath); } + return void 0; } -async function restoreActionCache(cacheRoot, cacheKey, restoreKeys) { - if (!isFeatureAvailable()) { - info("Actions cache service is not available; using local disk cache only."); - return void 0; +function findI386Library(libDirs) { + for (const dir of libDirs) { + if (true === containsArchSegment(dir, ["i386", "x86"]) && false === containsArchSegment(dir, ["amd64", "x64"])) { + return dir; + } } - try { - info(`Restoring WDK7 cache with key '${cacheKey}'.`); - const hit = await restoreCache([cacheRoot], cacheKey, restoreKeys); - if (hit) { - info(`Restored WDK7 cache from key '${hit}'.`); - } else { - info("No WDK7 actions/cache entry was restored."); + for (const dir of libDirs) { + if (false === containsArchSegment(dir, ["amd64", "x64"])) { + return dir; } - return hit; - } catch (error2) { - warning(`WDK7 cache restore failed: ${error2 instanceof Error ? error2.message : String(error2)}`); - return void 0; } + return void 0; } -async function saveActionCache(cacheRoot, cacheKey) { - if (!isFeatureAvailable()) { - info("Actions cache service is not available; skipping WDK7 cache save."); - return; +function debuggerRootFromIncludeDir(includeDir) { + let debuggerRoot = path16.dirname(includeDir); + if ("sdk" === path16.basename(debuggerRoot).toLowerCase()) { + debuggerRoot = path16.dirname(debuggerRoot); } - try { - info(`Saving WDK7 cache with key '${cacheKey}'.`); - await saveCache2([cacheRoot], cacheKey); - } catch (error2) { - warning(`WDK7 cache save skipped: ${error2 instanceof Error ? error2.message : String(error2)}`); + return debuggerRoot; +} +function findDebuggersMsiFiles(mediaRoot) { + return listFilesUnder(mediaRoot, isDebuggersMsiFile); +} +function isDbgEngHeader(filePath) { + return "dbgeng.h" === path16.basename(filePath).toLowerCase(); +} +function isDbgEngLibrary(filePath) { + return "dbgeng.lib" === path16.basename(filePath).toLowerCase(); +} +function isDebuggersMsiFile(filePath) { + const lower = filePath.toLowerCase(); + const baseName = path16.basename(lower); + if (false === baseName.endsWith(".msi")) { + return false; } + return baseName.startsWith("dbg") || lower.includes("debuggingtools") || lower.includes("debuggers"); +} +function containsArchSegment(value, archNames) { + const normalized = value.replace(/\\/g, "/").toLowerCase(); + const segments = normalized.split("/"); + for (const segment of segments) { + for (const archName of archNames) { + if (segment === archName) { + return true; + } + } + } + return false; +} + +// src/outputs.ts +function publishStaticOutputs() { + setOutput("cmake-module-dir", cmakeModuleDir()); + setOutput("toolchain-file", toolchainFile()); + setOutput("ddkbuild-cmd", ddkbuildCmd()); + setOutput("cmake-generator", cmakeGenerator); +} +function publishWdk7(root, source, cacheHit, sdk) { + const resolvedRoot = fullPath(root); + const host = hostBin(resolvedRoot); + exportVariable("WDK7_ROOT", resolvedRoot); + exportVariable("W7BASE", resolvedRoot); + exportVariable("WDK7_HOST_BIN", host); + exportVariable("WDK7_CMAKE_MODULE_DIR", cmakeModuleDir()); + exportVariable("WDK7_CMAKE_TOOLCHAIN_FILE", toolchainFile()); + exportVariable("WDK7_DDKBUILD_CMD", ddkbuildCmd()); + exportVariable("WDK7_CMAKE_GENERATOR", cmakeGenerator); + addPath(host); + setOutput("found", "true"); + setOutput("root", resolvedRoot); + setOutput("source", source); + setOutput("cache-hit", true === cacheHit ? "true" : "false"); + publishDebuggersSdk(sdk); + info(`WDK7 ready: root='${resolvedRoot}' source='${source}'`); +} +function publishNotFound(reason) { + info(reason); + publishStaticOutputs(); + setOutput("found", "false"); + setOutput("root", ""); + setOutput("source", "none"); + setOutput("cache-hit", "false"); + publishDebuggersSdk(void 0); } function publishDebuggersSdk(sdk) { - if (!sdk) { + if (void 0 === sdk) { setOutput("dbgeng-found", "false"); setOutput("debuggers-root", ""); setOutput("dbgeng-include-dir", ""); @@ -68111,110 +68342,235 @@ function publishDebuggersSdk(sdk) { `WDK7 Debuggers SDK ready: root='${sdk.root}' include='${sdk.includeDir}' lib-i386='${sdk.libI386}' lib-amd64='${sdk.libAmd64}'` ); } -function publishWdk7(root, source, cacheHit, sdk) { - const resolvedRoot = fullPath(root); - const host = hostBin(resolvedRoot); - exportVariable("WDK7_ROOT", resolvedRoot); - exportVariable("W7BASE", resolvedRoot); - exportVariable("WDK7_HOST_BIN", host); - exportVariable("WDK7_CMAKE_MODULE_DIR", cmakeModuleDir()); - exportVariable("WDK7_CMAKE_TOOLCHAIN_FILE", toolchainFile()); - exportVariable("WDK7_DDKBUILD_CMD", ddkbuildCmd()); - exportVariable("WDK7_CMAKE_GENERATOR", cmakeGenerator); - addPath(host); - setOutput("found", "true"); - setOutput("root", resolvedRoot); - setOutput("source", source); - setOutput("cache-hit", cacheHit ? "true" : "false"); - publishDebuggersSdk(sdk); - info(`WDK7 ready: root='${resolvedRoot}' source='${source}'`); + +// src/wdk.ts +import { existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs"; +import * as path17 from "node:path"; +function isWdk7Root(root) { + if ("" === root.trim()) { + return false; + } + const resolved = fullPath(root); + const required = requiredWdk7Files(resolved); + for (const requiredPath of required) { + if (false === existsSync8(requiredPath)) { + return false; + } + } + return true; } -function publishNotFound(reason) { - info(reason); - publishStaticOutputs(); - setOutput("found", "false"); - setOutput("root", ""); - setOutput("source", "none"); - setOutput("cache-hit", "false"); - publishDebuggersSdk(void 0); +function findWdk7Root(requestedRoot, cacheRoot, includeCache) { + const candidates = []; + addCandidate(candidates, requestedRoot, "input"); + addCandidate(candidates, process.env.WDK7_ROOT, "environment"); + addCandidate(candidates, process.env.W7BASE, "environment"); + if (true === includeCache) { + addCacheCandidates(candidates, cacheRoot); + } + addCandidate(candidates, "C:\\WinDDK\\7600.16385.1", "default"); + addCandidate(candidates, "C:\\WinDDK\\7600.16385.win7_wdk.100208-1538", "default"); + for (const candidate of candidates) { + if (true === isWdk7Root(candidate.root)) { + return candidate; + } + } + return void 0; } -async function prepareOptionalDebuggersSdk(enabled2, wdkRoot, cacheRoot, downloadUrls) { - if (!enabled2) { - info("Debugging Tools SDK was not requested. Set debugger: true to prepare DbgEng headers and libraries."); - return { cacheChanged: false }; +function findWdk7RootUnder(basePath) { + const resolvedBase = fullPath(basePath); + if ("" === resolvedBase || false === existsSync8(resolvedBase)) { + return void 0; } - return prepareDebuggersSdk(wdkRoot, cacheRoot, downloadUrls); + if (true === isWdk7Root(resolvedBase)) { + return resolvedBase; + } + const stack = [resolvedBase]; + while (0 < stack.length) { + const current = stack.pop(); + if (void 0 === current) { + continue; + } + const entries = readDirectoryEntries2(current); + for (const entry of entries) { + const entryPath = path17.join(current, entry); + const stats = readStats2(entryPath); + if (void 0 === stats) { + continue; + } + if (true === stats.isDirectory()) { + stack.push(entryPath); + } else if ("setenv.bat" === entry.toLowerCase()) { + const root = path17.dirname(path17.dirname(entryPath)); + if (true === isWdk7Root(root)) { + return fullPath(root); + } + } + } + } + return void 0; +} +function findCachedWdk7Root(cacheRoot) { + const root = findWdk7RootUnder(cacheRoot); + if (void 0 === root) { + return void 0; + } + return { + root, + source: "cache" + }; +} +function requiredWdk7Files(root) { + const required = [ + path17.join(root, "bin", "setenv.bat"), + path17.join(root, "inc", "api"), + path17.join(root, "inc", "ddk"), + path17.join(hostBin(root), "nmake.exe"), + path17.join(hostBin(root), "rc.exe") + ]; + for (const bin of targetBins(root)) { + required.push(path17.join(bin, "cl.exe")); + required.push(path17.join(bin, "link.exe")); + } + return required; +} +function addCacheCandidates(candidates, cacheRoot) { + addCandidate(candidates, path17.join(cacheRoot, "7600.16385.1"), "cache"); + addCandidate(candidates, path17.join(cacheRoot, "7600.16385.win7_wdk.100208-1538"), "cache"); + addCandidate(candidates, path17.join(cacheRoot, "wdk7", "7600.16385.1"), "cache"); + addCandidate(candidates, path17.join(cacheRoot, "wdk7", "7600.16385.win7_wdk.100208-1538"), "cache"); +} +function addCandidate(candidates, root, source) { + if (void 0 === root || "" === root.trim()) { + return; + } + const resolved = fullPath(root); + const key = resolved.toLowerCase(); + for (const candidate of candidates) { + if (key === candidate.root.toLowerCase()) { + return; + } + } + candidates.push({ + root: resolved, + source + }); } +function readDirectoryEntries2(directory) { + try { + return readdirSync3(directory); + } catch { + return []; + } +} +function readStats2(filePath) { + try { + return statSync3(filePath); + } catch { + return void 0; + } +} + +// src/main.ts async function run() { - if (process.platform !== "win32") { + if ("win32" !== process.platform) { throw new Error("wdk7 only runs on Windows."); } const inputs = readInputs(); const cacheRoot = defaultCacheRoot(); - const cacheKey = inputs.debugger ? debuggerCacheKey : wdkOnlyCacheKey; - const restoreKeys = inputs.debugger ? [wdkOnlyCacheKey] : []; - mkdirSync(cacheRoot, { recursive: true }); + const cacheKey = cacheKeyForDebugger(inputs.debugger); + const restoreKeys = restoreKeysForDebugger(inputs.debugger); + const cache = new CacheSession(cacheRoot, cacheKey, restoreKeys); + mkdirSync4(cacheRoot, { recursive: true }); publishStaticOutputs(); - let restoredCacheKey; - let cacheRestoreAttempted = false; - const restoreCacheOnce = async () => { - if (!cacheRestoreAttempted) { - cacheRestoreAttempted = true; - restoredCacheKey = await restoreActionCache(cacheRoot, cacheKey, restoreKeys); - } - }; const installed = findWdk7Root(inputs.root, cacheRoot, false); - if (installed) { - let sdk = inputs.debugger ? findDbgEngSdk(installed.root, cacheRoot) : void 0; - let cacheChanged = false; - if (inputs.debugger && !sdk) { - await restoreCacheOnce(); - sdk = findDbgEngSdk(installed.root, cacheRoot); - } - if (inputs.debugger && !sdk) { - const prepared2 = await prepareOptionalDebuggersSdk(true, installed.root, cacheRoot, inputs.downloadUrls); - sdk = prepared2.sdk; - cacheChanged = prepared2.cacheChanged; - } - publishWdk7(installed.root, installed.source, Boolean(restoredCacheKey), sdk); + if (void 0 !== installed) { + await useInstalledRoot(inputs, installed, cache, cacheRoot); return; } - await restoreCacheOnce(); - const found = findWdk7Root(inputs.root, cacheRoot, true) ?? findCachedWdk7Root(cacheRoot); - if (found) { - const prepared2 = await prepareOptionalDebuggersSdk(inputs.debugger, found.root, cacheRoot, inputs.downloadUrls); - if (restoredCacheKey !== cacheKey) { - await saveActionCache(cacheRoot, cacheKey); - } - publishWdk7( - found.root, - found.source, - found.source === "cache" || Boolean(restoredCacheKey), - prepared2.sdk - ); + await cache.restoreOnce(); + let found = findWdk7Root(inputs.root, cacheRoot, true); + if (void 0 === found) { + found = findCachedWdk7Root(cacheRoot); + } + if (void 0 !== found) { + await useCachedRoot(inputs, found, cache, cacheRoot); return; } - if (inputs.downloadUrls.length === 0) { + await downloadAndUseRoot(inputs, cache, cacheRoot); +} +async function useInstalledRoot(inputs, installed, cache, cacheRoot) { + const prepared = await prepareInstalledDebuggers(inputs, installed.root, cache, cacheRoot); + await cache.saveWhenChanged(prepared.cacheChanged); + publishWdk7(installed.root, installed.source, cache.hasRestored(), prepared.sdk); +} +async function useCachedRoot(inputs, found, cache, cacheRoot) { + const prepared = await prepareOptionalDebuggersSdk( + inputs.debugger, + found.root, + cacheRoot, + inputs.downloadUrls + ); + await cache.saveIfDifferentKey(); + publishWdk7( + found.root, + found.source, + "cache" === found.source || true === cache.hasRestored(), + prepared.sdk + ); +} +async function downloadAndUseRoot(inputs, cache, cacheRoot) { + if (0 === inputs.downloadUrls.length) { publishNotFound("WDK7 was not found and no download URLs are configured."); return; } const isoPath = await ensureWdk7Iso(cacheRoot, inputs.downloadUrls); - const targetRoot = path12.join(cacheRoot, "7600.16385.1"); - if (!isWdk7Root(targetRoot)) { + const targetRoot = path18.join(cacheRoot, "7600.16385.1"); + if (false === isWdk7Root(targetRoot)) { await installWdk7FromIso(isoPath, targetRoot); } - const resolvedRoot = findWdk7RootUnder(targetRoot) ?? findWdk7RootUnder("C:\\WinDDK"); - if (!resolvedRoot) { - throw new Error("WDK7 extraction completed, but no valid WDK7 root was found."); + let resolvedRoot = findWdk7RootUnder(targetRoot); + if (void 0 === resolvedRoot) { + resolvedRoot = findWdk7RootUnder("C:\\WinDDK"); } - const prepared = await prepareOptionalDebuggersSdk(inputs.debugger, resolvedRoot, cacheRoot, inputs.downloadUrls); - if (restoredCacheKey !== cacheKey) { - await saveActionCache(cacheRoot, cacheKey); + if (void 0 === resolvedRoot) { + throw new Error("WDK7 extraction completed, but no valid WDK7 root was found."); } + const prepared = await prepareOptionalDebuggersSdk( + inputs.debugger, + resolvedRoot, + cacheRoot, + inputs.downloadUrls + ); + await cache.saveIfDifferentKey(); publishWdk7(resolvedRoot, "download", false, prepared.sdk); } -run().catch((error2) => { - publishNotFound(`wdk7 failed: ${error2 instanceof Error ? error2.message : String(error2)}`); +async function prepareInstalledDebuggers(inputs, wdkRoot, cache, cacheRoot) { + if (false === inputs.debugger) { + return { cacheChanged: false }; + } + let sdk = findDbgEngSdk(wdkRoot, cacheRoot); + if (void 0 !== sdk) { + return { + sdk, + cacheChanged: false + }; + } + await cache.restoreOnce(); + sdk = findDbgEngSdk(wdkRoot, cacheRoot); + if (void 0 !== sdk) { + return { + sdk, + cacheChanged: false + }; + } + return prepareDebuggersSdk(wdkRoot, cacheRoot, inputs.downloadUrls); +} +run().catch(function onRunError(error2) { + if (error2 instanceof Error) { + publishNotFound(`wdk7 failed: ${error2.message}`); + return; + } + publishNotFound(`wdk7 failed: ${String(error2)}`); }); /*! Bundled license information: diff --git a/package-lock.json b/package-lock.json index a7ecdf2..dcf9c63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "setup-wdk7-action", + "name": "setup-wdk7", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "setup-wdk7-action", + "name": "setup-wdk7", "version": "1.0.0", "dependencies": { "@actions/cache": "^6.0.1", diff --git a/package.json b/package.json index 63033d1..b63b84e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "setup-wdk7-action", + "name": "setup-wdk7", "version": "1.0.0", "private": true, "type": "module", diff --git a/scripts/dismount-iso.ps1 b/scripts/dismount-iso.ps1 index 675f42d..470eaf0 100644 --- a/scripts/dismount-iso.ps1 +++ b/scripts/dismount-iso.ps1 @@ -5,4 +5,8 @@ param( ) $ErrorActionPreference = "SilentlyContinue" + +# Dismount is intentionally best-effort because callers run this from finally +# blocks. A missing or already-dismounted image should not hide the original +# installation or extraction error. Dismount-DiskImage -ImagePath $ImagePath diff --git a/scripts/mount-iso.ps1 b/scripts/mount-iso.ps1 index 4d373eb..bfef0f0 100644 --- a/scripts/mount-iso.ps1 +++ b/scripts/mount-iso.ps1 @@ -6,6 +6,9 @@ param( $ErrorActionPreference = "Stop" +# Mount-DiskImage returns before every runner reliably exposes the volume. +# The short wait avoids a race where Get-Volume sees the disk object but not the +# assigned drive letter yet. $mount = Mount-DiskImage -ImagePath $ImagePath -PassThru Start-Sleep -Seconds 2 diff --git a/scripts/test-e2e.ps1 b/scripts/test-e2e.ps1 new file mode 100644 index 0000000..7dd24b5 --- /dev/null +++ b/scripts/test-e2e.ps1 @@ -0,0 +1,237 @@ +[CmdletBinding()] +param( + [ValidateSet("default", "debugger")] + [string]$Mode = "default" +) + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = "Stop" + +$repoRoot = Split-Path -Parent $PSScriptRoot +Set-Location $repoRoot + +<# +.SYNOPSIS +Ensures a required action output path exists before a build step uses it. + +.DESCRIPTION +The e2e script depends on environment variables exported by the action. Failing +early with a direct message makes CI errors easier to diagnose than letting +CMake or ddkbuild fail with a missing tool path later. +#> +function Assert-ExistingPath { + param( + [string]$Path, + [string]$Message + ) + + # Empty strings and missing files are treated the same because both mean the + # action did not provide a usable path for the next build step. + if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path)) { + throw $Message + } +} + +<# +.SYNOPSIS +Runs a command and exits with the command's status when it fails. + +.DESCRIPTION +PowerShell does not automatically fail scripts for native command exit codes. +This wrapper keeps each e2e command explicit while preserving the original +native exit code for CI. +#> +function Invoke-Checked { + param( + [string]$FilePath, + [string[]]$Arguments = @() + ) + + # Native tools such as CMake, cmd, and ddkbuild communicate failure through + # LASTEXITCODE rather than PowerShell exceptions. + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} + +<# +.SYNOPSIS +Removes a generated e2e build directory inside the repository. + +.DESCRIPTION +The script creates fresh build trees so stale CMake output cannot satisfy a +missing-output assertion. The repository-boundary check prevents accidental +recursive deletion outside the workspace. +#> +function Clear-BuildDirectory { + param([string]$Path) + + $resolvedRepo = [System.IO.Path]::GetFullPath($repoRoot).TrimEnd("\") + $resolvedPath = [System.IO.Path]::GetFullPath($Path) + $repoPrefix = "$resolvedRepo\" + + # The path guard matters because this function performs recursive deletion + # before every CMake configure. + if ($resolvedPath -ne $resolvedRepo -and -not $resolvedPath.StartsWith($repoPrefix, [System.StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to remove a build directory outside the repository: $resolvedPath" + } + + if (Test-Path -LiteralPath $resolvedPath) { + Remove-Item -LiteralPath $resolvedPath -Recurse -Force + } +} + +<# +.SYNOPSIS +Builds the user-mode CMake fixture for both WDK7 architectures. + +.DESCRIPTION +The fixture covers executables, DLLs, static libraries, FetchContent, and the +optional DbgEng target. This keeps the workflow YAML small while preserving the +same coverage as the previous inline CI script. +#> +function Build-CmakeUserTargets { + param( + [string]$BuildLabel, + [bool]$ExpectDbgEng + ) + + $source = Join-Path $repoRoot "test\e2e\cmake" + $expected = @( + "e2e_cli.exe", + "e2e_dll.dll", + "e2e_lib.lib", + "e2e_native.exe" + ) + + if ($ExpectDbgEng) { + $expected += "e2e_dbgeng.exe" + } + + foreach ($arch in @("i386", "amd64")) { + $build = Join-Path $repoRoot ".e2e\$BuildLabel-$arch" + + # A clean build directory makes missing target assertions meaningful. + Clear-BuildDirectory $build + + Invoke-Checked "cmake" @( + "-S", $source, + "-B", $build, + "-G", $env:WDK7_CMAKE_GENERATOR, + "-DCMAKE_TOOLCHAIN_FILE=$env:WDK7_CMAKE_TOOLCHAIN_FILE", + "-DWDK7_ARCH=$arch", + "-DCMAKE_BUILD_TYPE=Release" + ) + + Invoke-Checked "cmake" @("--build", $build, "--config", "Release") + + # The fixture validates artifacts instead of only trusting command exit + # codes because a toolchain regression can silently skip a target. + foreach ($file in $expected) { + $path = Join-Path $build $file + Assert-ExistingPath $path "Missing CMake $arch output: $path" + } + + $dbgengPath = Join-Path $build "e2e_dbgeng.exe" + + # The default setup must not leak Debugging Tools paths into normal WDK + # builds. This assertion protects the debugger opt-in contract. + if (-not $ExpectDbgEng -and (Test-Path -LiteralPath $dbgengPath)) { + throw "DbgEng target was built without debugger=true: $dbgengPath" + } + } +} + +<# +.SYNOPSIS +Builds the kernel-mode CMake fixture for both WDK7 architectures. + +.DESCRIPTION +The sys fixture verifies that the bundled toolchain can switch into kernel mode +with standard CMake target commands. +#> +function Build-CmakeSysTargets { + $source = Join-Path $repoRoot "test\e2e\cmake-sys" + + foreach ($arch in @("i386", "amd64")) { + $build = Join-Path $repoRoot ".e2e\cmake-sys-$arch" + + # Kernel builds are sensitive to cached linker state, so each arch gets + # a freshly configured build tree. + Clear-BuildDirectory $build + + Invoke-Checked "cmake" @( + "-S", $source, + "-B", $build, + "-G", $env:WDK7_CMAKE_GENERATOR, + "-DCMAKE_TOOLCHAIN_FILE=$env:WDK7_CMAKE_TOOLCHAIN_FILE", + "-DWDK7_ARCH=$arch", + "-DWDK7_DEFAULT_MODE=KERNEL", + "-DCMAKE_BUILD_TYPE=Release" + ) + + Invoke-Checked "cmake" @("--build", $build, "--config", "Release") + + $path = Join-Path $build "e2e_sys.sys" + + # A successful command is not enough; the expected driver file must be + # present for the fixture to prove the linker mode worked. + Assert-ExistingPath $path "Missing CMake $arch sys output: $path" + } +} + +<# +.SYNOPSIS +Builds the legacy ddkbuild fixture for x86 and amd64. + +.DESCRIPTION +Legacy WDK projects commonly rely on ddkbuild.cmd rather than CMake. This +fixture protects the compatibility wrapper that the action exports. +#> +function Build-DdkbuildTarget { + $source = "test\e2e\ddkbuild\sys" + $sourcePath = Join-Path $repoRoot $source + + foreach ($target in @("-WIN7", "-WIN7A64")) { + # cmd.exe is used because ddkbuild.cmd sets batch-local environment and + # expects normal cmd call semantics. The source path stays relative + # because legacy ddkbuild path handling is more reliable with repo-local + # target directories than with absolute workflow paths. + Invoke-Checked "cmd" @("/s", "/c", "call ""$env:WDK7_DDKBUILD_CMD"" $target free ""$source""") + } + + $sysFiles = @(Get-ChildItem -Path $sourcePath -Recurse -Filter e2e_ddkbuild.sys) + + # Both architectures should produce a driver. Counting outputs catches a + # regression where one target overwrites or skips the other. + if ($sysFiles.Count -lt 2) { + throw "Expected ddkbuild to produce x86 and amd64 .sys files." + } + + $sysFiles | ForEach-Object { Write-Host $_.FullName } +} + +Assert-ExistingPath $env:WDK7_ROOT "WDK7_ROOT is not set or does not exist." +Assert-ExistingPath $env:WDK7_CMAKE_TOOLCHAIN_FILE "WDK7_CMAKE_TOOLCHAIN_FILE is not set or does not exist." +Assert-ExistingPath $env:WDK7_DDKBUILD_CMD "WDK7_DDKBUILD_CMD is not set or does not exist." + +if ([string]::IsNullOrWhiteSpace($env:WDK7_CMAKE_GENERATOR)) { + throw "WDK7_CMAKE_GENERATOR is not set." +} + +if ($Mode -eq "debugger") { + # Debugger mode must prove the action exported the SDK paths before the CMake + # fixture attempts to link the DbgEng target. + Assert-ExistingPath $env:WDK7_DBGENG_INCLUDE_DIR "WDK7_DBGENG_INCLUDE_DIR is not set or does not exist." + Assert-ExistingPath $env:WDK7_DBGENG_LIB_I386 "WDK7_DBGENG_LIB_I386 is not set or does not exist." + Assert-ExistingPath $env:WDK7_DBGENG_LIB_AMD64 "WDK7_DBGENG_LIB_AMD64 is not set or does not exist." + Build-CmakeUserTargets "cmake-dbgeng" $true +} else { + # The default mode intentionally clears debugger variables so the fixture can + # verify that Debugging Tools are not prepared unless explicitly requested. + Remove-Item -LiteralPath Env:WDK7_DBGENG_INCLUDE_DIR, Env:WDK7_DBGENG_LIB_I386, Env:WDK7_DBGENG_LIB_AMD64 -Force -ErrorAction SilentlyContinue + Build-CmakeUserTargets "cmake-basic" $false + Build-CmakeSysTargets + Build-DdkbuildTarget +} diff --git a/scripts/test-local.ps1 b/scripts/test-local.ps1 index 431213e..f60cfd3 100644 --- a/scripts/test-local.ps1 +++ b/scripts/test-local.ps1 @@ -12,6 +12,9 @@ $repoRoot = Split-Path -Parent $PSScriptRoot $debugRoot = Join-Path $repoRoot ".action-debug\$PID" New-Item -ItemType Directory -Force -Path $debugRoot | Out-Null +# The action writes to GitHub-provided files during real workflow runs. Local +# debugging uses temporary files with the same environment variable names so the +# TypeScript entry point exercises the same output path. $env:GITHUB_ACTION_PATH = $repoRoot $env:GITHUB_OUTPUT = Join-Path $debugRoot "github-output.txt" $env:GITHUB_ENV = Join-Path $debugRoot "github-env.txt" @@ -24,6 +27,8 @@ Set-Item -Path "Env:INPUT_ROOT" -Value $Root Set-Item -Path "Env:INPUT_DOWNLOAD-URL" -Value $DownloadUrl Set-Item -Path "Env:INPUT_DEBUGGER" -Value ($(if ($Debugger) { "true" } else { "false" })) +# The local harness runs the bundled action because that is the file GitHub will +# execute. This catches stale dist/index.js output before a release. $entry = Join-Path $repoRoot "dist\index.js" if (-not (Test-Path -LiteralPath $entry)) { throw "Missing dist\index.js. Run 'npm.cmd install' and 'npm.cmd run build' first." diff --git a/scripts/trim-dist.cjs b/scripts/trim-dist.cjs index a540e7b..6d792c0 100644 --- a/scripts/trim-dist.cjs +++ b/scripts/trim-dist.cjs @@ -1,14 +1,59 @@ const fs = require("node:fs"); -const filePath = process.argv[2]; -if (!filePath) { - throw new Error("Usage: node scripts/trim-dist.cjs "); +/** + * main validates CLI arguments and applies the distribution-file cleanup. + * Keeping the entry point explicit makes this small utility easier to extend + * without hiding behavior in top-level expressions. + */ +function main() { + const filePath = process.argv[2]; + + if (!filePath) { + throw new Error("Usage: node scripts/trim-dist.cjs "); + } + + trimFile(filePath); +} + +/** + * trimFile rewrites a bundled JavaScript file with stable whitespace. The + * generated action bundle is committed, so deterministic whitespace keeps diffs + * focused on real code changes. + */ +function trimFile(filePath) { + const original = fs.readFileSync(filePath, "utf8"); + const trimmed = trimTrailingWhitespace(original); + const normalized = ensureTrailingNewline(trimmed); + + fs.writeFileSync(filePath, normalized, "utf8"); } -const original = fs.readFileSync(filePath, "utf8"); -const trimmed = original - .split(/\r?\n/) - .map(line => line.replace(/[ \t]+$/u, "")) - .join("\n"); +/** + * trimTrailingWhitespace removes line-end spaces without changing line order. + * The explicit loop avoids a compact callback pipeline in a maintenance script + * that contributors may need to adjust during release work. + */ +function trimTrailingWhitespace(content) { + const lines = content.split(/\r?\n/); + const trimmedLines = []; + + for (const line of lines) { + trimmedLines.push(line.replace(/[ \t]+$/u, "")); + } + + return trimmedLines.join("\n"); +} + +/** + * ensureTrailingNewline preserves the normal text-file convention used by the + * repository. Build tools can otherwise create noisy one-byte diffs. + */ +function ensureTrailingNewline(content) { + if (content.endsWith("\n")) { + return content; + } + + return `${content}\n`; +} -fs.writeFileSync(filePath, trimmed.endsWith("\n") ? trimmed : `${trimmed}\n`, "utf8"); +main(); diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..769d2d7 --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,140 @@ +import * as actionsCache from "@actions/cache"; +import * as core from "@actions/core"; + +/** + * CacheSession tracks one Actions cache restore/save lifecycle. The action may + * need to restore only once and then decide later whether the cache should be + * saved, so this class keeps that state explicit instead of passing mutable + * variables through the main flow. + */ +export class CacheSession { + private root: string; + private key: string; + private restoreKeys: string[]; + private restoredKey: string | undefined; + private restoreAttempted: boolean; + + /** + * The constructor stores cache identity without touching the cache service. + * Deferring network calls keeps the main flow free to skip restore when a + * valid local WDK installation already satisfies the request. + */ + public constructor(root: string, key: string, restoreKeys: string[]) { + this.root = root; + this.key = key; + this.restoreKeys = restoreKeys; + this.restoredKey = undefined; + this.restoreAttempted = false; + } + + /** + * restoreOnce lazily restores the cache and returns the hit key. Repeated + * calls are common in the debugger flow, and only the first one should contact + * the cache service. + */ + public async restoreOnce(): Promise { + if (false === this.restoreAttempted) { + this.restoreAttempted = true; + this.restoredKey = await restoreActionCache(this.root, this.key, this.restoreKeys); + } + + return this.restoredKey; + } + + /** + * hasRestored reports whether any Actions cache entry was reused. The action + * exposes this as a user-facing cache-hit signal. + */ + public hasRestored(): boolean { + return undefined !== this.restoredKey; + } + + /** + * saveIfDifferentKey stores the cache when the exact requested key was not + * already restored. This also upgrades a base WDK cache into the debugger + * cache after Debugging Tools are prepared. + */ + public async saveIfDifferentKey(): Promise { + if (this.key === this.restoredKey) { + return; + } + + await saveActionCache(this.root, this.key); + } + + /** + * saveWhenChanged is used for flows that started from a local WDK install. + * They should not save the local WDK tree, but should preserve newly extracted + * debugger files placed under the action cache root. + */ + public async saveWhenChanged(changed: boolean): Promise { + if (false === changed) { + return; + } + + await this.saveIfDifferentKey(); + } +} + +/** + * restoreActionCache wraps @actions/cache restore behavior with non-fatal error + * handling. Cache service outages should slow WDK setup down, not break a build + * that can still download or use local disk state. + */ +async function restoreActionCache(cacheRoot: string, cacheKey: string, restoreKeys: string[]): Promise { + if (false === actionsCache.isFeatureAvailable()) { + core.info("Actions cache service is not available; using local disk cache only."); + + return undefined; + } + + try { + core.info(`Restoring WDK7 cache with key '${cacheKey}'.`); + + const hit: string | undefined = await actionsCache.restoreCache([cacheRoot], cacheKey, restoreKeys); + + if (undefined !== hit) { + core.info(`Restored WDK7 cache from key '${hit}'.`); + } else { + core.info("No WDK7 actions/cache entry was restored."); + } + + return hit; + } catch (error) { + core.warning(`WDK7 cache restore failed: ${formatError(error)}`); + + return undefined; + } +} + +/** + * saveActionCache wraps @actions/cache save behavior with non-fatal error + * handling. Cache save races are expected in CI matrices, and a skipped save + * should not invalidate a successful build. + */ +async function saveActionCache(cacheRoot: string, cacheKey: string): Promise { + if (false === actionsCache.isFeatureAvailable()) { + core.info("Actions cache service is not available; skipping WDK7 cache save."); + + return; + } + + try { + core.info(`Saving WDK7 cache with key '${cacheKey}'.`); + await actionsCache.saveCache([cacheRoot], cacheKey); + } catch (error) { + core.warning(`WDK7 cache save skipped: ${formatError(error)}`); + } +} + +/** + * formatError keeps cache warnings readable even when an external library throws + * a non-Error value. + */ +function formatError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return String(error); +} diff --git a/src/debuggers.ts b/src/debuggers.ts new file mode 100644 index 0000000..d352b18 --- /dev/null +++ b/src/debuggers.ts @@ -0,0 +1,472 @@ +import * as core from "@actions/core"; +import { existsSync, mkdirSync } from "node:fs"; +import * as path from "node:path"; + +import { ensureWdk7Iso } from "./download.js"; +import { listFilesUnder } from "./files.js"; +import { dismountIso, extractMsi, mountIso } from "./iso.js"; +import { uniqueStrings } from "./lists.js"; +import { fullPath } from "./paths.js"; +import type { DebuggersSdk, PreparedDebuggers } from "./types.js"; + +interface DebuggersLayout { + root: string; + includeDir: string; + libI386: string; + libAmd64: string; +} + +/** + * findDbgEngSdk searches the WDK root first, then the cache root. This order + * prefers files tied to the selected WDK installation while still supporting + * separately cached Debugging Tools extraction. + */ +export function findDbgEngSdk(wdkRoot: string, cacheRoot: string): DebuggersSdk | undefined { + const sdkFromWdk: DebuggersSdk | undefined = findDbgEngSdkUnder(wdkRoot); + + if (undefined !== sdkFromWdk) { + return sdkFromWdk; + } + + return findDbgEngSdkUnder(cacheRoot); +} + +/** + * prepareOptionalDebuggersSdk keeps the main flow explicit about the debugger + * opt-in. Normal WDK setup should not pay for Debugging Tools discovery or + * extraction unless the workflow requested it. + */ +export async function prepareOptionalDebuggersSdk( + enabled: boolean, + wdkRoot: string, + cacheRoot: string, + downloadUrls: string[] +): Promise { + if (false === enabled) { + core.info("Debugging Tools SDK was not requested. Set debugger: true to prepare DbgEng headers and libraries."); + + return { cacheChanged: false }; + } + + return prepareDebuggersSdk(wdkRoot, cacheRoot, downloadUrls); +} + +/** + * prepareDebuggersSdk finds or extracts the optional DbgEng SDK. It reports + * cache changes separately so callers can avoid saving unchanged local WDK + * installations. + */ +export async function prepareDebuggersSdk( + wdkRoot: string, + cacheRoot: string, + downloadUrls: string[] +): Promise { + let sdk: DebuggersSdk | undefined = findDbgEngSdk(wdkRoot, cacheRoot); + + if (undefined !== sdk) { + return { + sdk: sdk, + cacheChanged: false + }; + } + + if (0 === downloadUrls.length) { + core.info("WDK7 Debuggers SDK was not found and no download URLs are configured."); + + return { cacheChanged: false }; + } + + const isoPath: string = await ensureWdk7Iso(cacheRoot, downloadUrls); + const changed: boolean = await installDebuggersFromIso(isoPath, wdkRoot, cacheRoot); + + sdk = findDbgEngSdk(wdkRoot, cacheRoot); + + if (undefined === sdk) { + core.info("WDK7 Debuggers SDK was not found after Debugging Tools extraction."); + } + + return { + sdk: sdk, + cacheChanged: changed + }; +} + +/** + * hasDbgEngInclude checks the header that proves an include directory exposes + * the Debugging Tools SDK API. + */ +function hasDbgEngInclude(includeDir: string): boolean { + return existsSync(path.join(includeDir, "DbgEng.h")); +} + +/** + * hasDbgEngLibraries checks the paired libraries used by DbgEng consumers. Both + * files are required because linking dbgeng-only programs commonly also needs + * dbghelp.lib from the same SDK layout. + */ +function hasDbgEngLibraries(libraryDir: string): boolean { + return existsSync(path.join(libraryDir, "dbgeng.lib")) && + existsSync(path.join(libraryDir, "dbghelp.lib")); +} + +/** + * debuggerBin resolves optional Debugging Tools executable directories. These + * paths are exported when available but are not required for SDK-only builds. + */ +function debuggerBin(root: string, arch: "x86" | "x64"): string { + const candidates: string[] = [ + path.join(root, arch), + path.join(root, "Debuggers", arch), + path.join(root, "Debugging Tools for Windows", arch), + path.join(root, `Debugging Tools for Windows (${arch})`) + ]; + + for (const candidate of candidates) { + if (true === existsSync(candidate)) { + return candidate; + } + } + + return ""; +} + +/** + * createDebuggersSdk validates a possible SDK layout before exposing it. This + * keeps all layout probes honest: a directory is accepted only when the header + * and both architecture libraries are present. + */ +function createDebuggersSdk( + root: string, + includeDir: string, + libI386: string, + libAmd64: string +): DebuggersSdk | undefined { + if (false === hasDbgEngInclude(includeDir)) { + return undefined; + } + + if (false === hasDbgEngLibraries(libI386)) { + return undefined; + } + + if (false === hasDbgEngLibraries(libAmd64)) { + return undefined; + } + + const resolvedRoot: string = fullPath(root); + + return { + root: resolvedRoot, + includeDir: fullPath(includeDir), + libI386: fullPath(libI386), + libAmd64: fullPath(libAmd64), + binX86: debuggerBin(resolvedRoot, "x86"), + binX64: debuggerBin(resolvedRoot, "x64") + }; +} + +/** + * findDbgEngSdkInKnownLayouts checks common installer layouts before scanning. + * Direct layout checks are easier to understand and faster than recursively + * searching every restored cache. + */ +function findDbgEngSdkInKnownLayouts(root: string): DebuggersSdk | undefined { + const resolved: string = fullPath(root); + + if ("" === resolved || false === existsSync(resolved)) { + return undefined; + } + + const layouts: DebuggersLayout[] = knownLayouts(resolved); + + for (const layout of layouts) { + const sdk: DebuggersSdk | undefined = createDebuggersSdk( + layout.root, + layout.includeDir, + layout.libI386, + layout.libAmd64 + ); + + if (undefined !== sdk) { + return sdk; + } + } + + return undefined; +} + +/** + * findDbgEngSdkUnder scans for DbgEng headers and matching architecture library + * directories when known layouts fail. This supports unusual administrative MSI + * extraction trees without hardcoding every possible path. + */ +function findDbgEngSdkUnder(root: string): DebuggersSdk | undefined { + const known: DebuggersSdk | undefined = findDbgEngSdkInKnownLayouts(root); + + if (undefined !== known) { + return known; + } + + const includeFiles: string[] = findDbgEngHeaders(root); + + if (0 === includeFiles.length) { + return undefined; + } + + const libDirs: string[] = findDbgEngLibraryDirs(root); + + if (0 === libDirs.length) { + return undefined; + } + + const amd64Lib: string | undefined = findAmd64Library(libDirs); + const i386Lib: string | undefined = findI386Library(libDirs); + + if (undefined === i386Lib || undefined === amd64Lib) { + return undefined; + } + + for (const includeFile of includeFiles) { + const includeDir: string = path.dirname(includeFile); + const debuggerRoot: string = debuggerRootFromIncludeDir(includeDir); + const sdk: DebuggersSdk | undefined = createDebuggersSdk(debuggerRoot, includeDir, i386Lib, amd64Lib); + + if (undefined !== sdk) { + return sdk; + } + } + + return undefined; +} + +/** + * installDebuggersFromIso extracts Debugging Tools MSI packages into both the + * selected WDK root and the cache root. Some builds expect the SDK beside WDK7, + * while the cache copy survives future clean runners. + */ +async function installDebuggersFromIso(isoPath: string, wdkRoot: string, cacheRoot: string): Promise { + core.info(`Mounting WDK7 ISO for Debugging Tools: ${isoPath}`); + + const drive: string = await mountIso(isoPath); + + try { + const mediaRoot: string = `${drive}:\\`; + const msiFiles: string[] = findDebuggersMsiFiles(mediaRoot); + + if (0 === msiFiles.length) { + core.info("No Debugging Tools MSI packages were found in the WDK7 ISO."); + + return false; + } + + let changed: boolean = false; + const targetRoots: string[] = uniqueStrings([ + wdkRoot, + path.join(cacheRoot, "debuggers") + ]); + + for (const targetRoot of targetRoots) { + mkdirSync(targetRoot, { recursive: true }); + + const logRoot: string = path.join(targetRoot, "_debuggers_install_logs"); + mkdirSync(logRoot, { recursive: true }); + + for (const msiPath of msiFiles) { + const extracted: boolean = await extractMsi(msiPath, targetRoot, logRoot); + + if (true === extracted) { + changed = true; + } + } + + if (undefined !== findDbgEngSdk(wdkRoot, cacheRoot)) { + return changed; + } + } + + return changed; + } finally { + await dismountIso(isoPath); + } +} + +/** + * knownLayouts returns the common SDK directory shapes produced by WDK7 and + * Debugging Tools installers. Keeping this data in a small function makes layout + * support easy to extend without touching discovery control flow. + */ +function knownLayouts(resolved: string): DebuggersLayout[] { + return [ + { + root: path.join(resolved, "Debuggers"), + includeDir: path.join(resolved, "Debuggers", "sdk", "inc"), + libI386: path.join(resolved, "Debuggers", "sdk", "lib", "i386"), + libAmd64: path.join(resolved, "Debuggers", "sdk", "lib", "amd64") + }, + { + root: resolved, + includeDir: path.join(resolved, "sdk", "inc"), + libI386: path.join(resolved, "sdk", "lib", "i386"), + libAmd64: path.join(resolved, "sdk", "lib", "amd64") + }, + { + root: path.join(resolved, "Debuggers"), + includeDir: path.join(resolved, "Debuggers", "inc"), + libI386: path.join(resolved, "Debuggers", "lib", "x86"), + libAmd64: path.join(resolved, "Debuggers", "lib", "x64") + }, + { + root: resolved, + includeDir: path.join(resolved, "inc"), + libI386: path.join(resolved, "lib", "x86"), + libAmd64: path.join(resolved, "lib", "x64") + }, + { + root: resolved, + includeDir: path.join(resolved, "Include"), + libI386: path.join(resolved, "Lib"), + libAmd64: path.join(resolved, "Lib", "x64") + } + ]; +} + +/** + * findDbgEngHeaders locates candidate DbgEng.h files. A named predicate keeps + * the scan criteria searchable and avoids burying SDK identity inside a callback. + */ +function findDbgEngHeaders(root: string): string[] { + return listFilesUnder(root, isDbgEngHeader); +} + +/** + * findDbgEngLibraryDirs returns unique directories that contain the required + * DbgEng library pair. The directory list, not the file list, is what the action + * later exports for linker use. + */ +function findDbgEngLibraryDirs(root: string): string[] { + const files: string[] = listFilesUnder(root, isDbgEngLibrary); + const dirs: string[] = []; + + for (const filePath of files) { + const dir: string = path.dirname(filePath); + + if (true === hasDbgEngLibraries(dir)) { + dirs.push(dir); + } + } + + return uniqueStrings(dirs); +} + +/** + * findAmd64Library selects the amd64/x64 library directory from scanned SDK + * candidates. Architecture hints live in directory names for these installers. + */ +function findAmd64Library(libDirs: string[]): string | undefined { + for (const dir of libDirs) { + if (true === containsArchSegment(dir, ["amd64", "x64"])) { + return dir; + } + } + + return undefined; +} + +/** + * findI386Library selects the x86 library directory while avoiding amd64 paths. + * Some SDK layouts use a generic lib directory for x86, so a non-amd64 fallback + * is kept after checking explicit i386/x86 names. + */ +function findI386Library(libDirs: string[]): string | undefined { + for (const dir of libDirs) { + if (true === containsArchSegment(dir, ["i386", "x86"]) && + false === containsArchSegment(dir, ["amd64", "x64"])) { + return dir; + } + } + + for (const dir of libDirs) { + if (false === containsArchSegment(dir, ["amd64", "x64"])) { + return dir; + } + } + + return undefined; +} + +/** + * debuggerRootFromIncludeDir walks from an include directory to the SDK root. + * WDK7 Debugging Tools commonly put headers under sdk/inc, and callers need the + * parent Debuggers directory as the exported SDK root. + */ +function debuggerRootFromIncludeDir(includeDir: string): string { + let debuggerRoot: string = path.dirname(includeDir); + + if ("sdk" === path.basename(debuggerRoot).toLowerCase()) { + debuggerRoot = path.dirname(debuggerRoot); + } + + return debuggerRoot; +} + +/** + * findDebuggersMsiFiles scans the mounted ISO for likely Debugging Tools MSI + * packages. The package names vary, so the predicate accepts both explicit dbg + * prefixes and descriptive directory names. + */ +function findDebuggersMsiFiles(mediaRoot: string): string[] { + return listFilesUnder(mediaRoot, isDebuggersMsiFile); +} + +/** + * isDbgEngHeader identifies the SDK header that proves an include directory is + * useful for DbgEng consumers. + */ +function isDbgEngHeader(filePath: string): boolean { + return "dbgeng.h" === path.basename(filePath).toLowerCase(); +} + +/** + * isDbgEngLibrary identifies dbgeng.lib files; the paired dbghelp.lib check is + * performed later at the directory level. + */ +function isDbgEngLibrary(filePath: string): boolean { + return "dbgeng.lib" === path.basename(filePath).toLowerCase(); +} + +/** + * isDebuggersMsiFile identifies mounted ISO packages that may contain Debugging + * Tools files. A broad match is needed because Microsoft used several naming + * conventions across SDK package layouts. + */ +function isDebuggersMsiFile(filePath: string): boolean { + const lower: string = filePath.toLowerCase(); + const baseName: string = path.basename(lower); + + if (false === baseName.endsWith(".msi")) { + return false; + } + + return baseName.startsWith("dbg") || + lower.includes("debuggingtools") || + lower.includes("debuggers"); +} + +/** + * containsArchSegment checks architecture names as path segments. Segment-aware + * matching avoids treating unrelated directory text as an architecture hint. + */ +function containsArchSegment(value: string, archNames: string[]): boolean { + const normalized: string = value.replace(/\\/g, "/").toLowerCase(); + const segments: string[] = normalized.split("/"); + + for (const segment of segments) { + for (const archName of archNames) { + if (segment === archName) { + return true; + } + } + } + + return false; +} diff --git a/src/download.ts b/src/download.ts new file mode 100644 index 0000000..1a342e3 --- /dev/null +++ b/src/download.ts @@ -0,0 +1,251 @@ +import * as core from "@actions/core"; +import { createWriteStream, existsSync, mkdirSync, renameSync, rmSync } from "node:fs"; +import * as http from "node:http"; +import * as https from "node:https"; +import * as path from "node:path"; + +import { downloadRetries } from "./settings.js"; + +const maxRedirects: number = 8; +const requestTimeoutMilliseconds: number = 300000; + +/** + * ensureWdk7Iso returns a cached ISO path or downloads one from the configured + * sources. The ISO is stored beside extracted WDK files so all expensive WDK7 + * artifacts share the same cache lifecycle. + */ +export async function ensureWdk7Iso(cacheRoot: string, urls: string[]): Promise { + const isoPath: string = path.join(cacheRoot, "GRMWDK_EN_7600_1.ISO"); + + if (true === existsSync(isoPath)) { + core.info(`Using cached WDK7 ISO: ${isoPath}`); + return isoPath; + } + + const downloadedUrl: string = await downloadFileFromUrlsWithRetries(urls, isoPath, downloadRetries); + core.info(`Downloaded WDK7 ISO from: ${downloadedUrl}`); + + return isoPath; +} + +/** + * downloadFileFromUrlsWithRetries tries each configured source in order. This + * keeps user-provided mirrors first while still falling back to the built-in + * Microsoft URL when a mirror is unavailable. + */ +async function downloadFileFromUrlsWithRetries( + urls: string[], + outputPath: string, + attempts: number +): Promise { + let lastError: unknown = undefined; + + for (let index: number = 0; index < urls.length; index = index + 1) { + const url: string = urls[index]; + + try { + core.info(`Downloading WDK7 ISO from source ${index + 1}/${urls.length}: ${url}`); + await downloadFileWithRetries(url, outputPath, attempts); + + return url; + } catch (error) { + lastError = error; + rmSync(outputPath, { force: true }); + + if (index + 1 < urls.length) { + core.warning(`WDK7 ISO source ${index + 1}/${urls.length} failed: ${formatError(error)}. Trying next source.`); + } + } + } + + throw errorFromUnknown(lastError); +} + +/** + * downloadFileWithRetries retries one URL with a short backoff. WDK ISO + * downloads are large enough that transient network failures are common, but + * retrying forever would hide broken workflow configuration. + */ +async function downloadFileWithRetries(urlText: string, outputPath: string, attempts: number): Promise { + let lastError: unknown = undefined; + + for (let attempt: number = 1; attempt <= attempts; attempt = attempt + 1) { + try { + await downloadFile(urlText, outputPath, 0); + return; + } catch (error) { + lastError = error; + rmSync(outputPath, { force: true }); + + if (attempt >= attempts) { + break; + } + + const delay: number = Math.min(30000, 2000 * attempt); + core.warning(`WDK7 ISO download attempt ${attempt}/${attempts} failed: ${formatError(error)}. Retrying in ${delay / 1000}s.`); + + await sleep(delay); + } + } + + throw errorFromUnknown(lastError); +} + +/** + * downloadFile streams one URL to disk and follows a limited number of + * redirects. The temporary file is renamed only after the stream closes so the + * cache never treats a partial ISO as valid. + */ +async function downloadFile(urlText: string, outputPath: string, redirectCount: number): Promise { + if (maxRedirects < redirectCount) { + throw new Error(`Too many redirects while downloading '${urlText}'.`); + } + + mkdirSync(path.dirname(outputPath), { recursive: true }); + + const url: URL = new URL(urlText); + const tmpPath: string = `${outputPath}.tmp`; + + rmSync(tmpPath, { force: true }); + + /** + * downloadFilePromise adapts the streaming HTTP client to async/await. The + * closure owns the temporary path so every failure path can remove the same + * partial file. + */ + function downloadFilePromise(resolve: () => void, reject: (reason?: unknown) => void): void { + /** + * onResponse validates HTTP status before writing to disk. Redirects are + * handled here because native http clients do not follow them automatically. + */ + function onResponse(response: http.IncomingMessage): void { + let status: number = 0; + + if (undefined !== response.statusCode) { + status = response.statusCode; + } + + const location: string | undefined = response.headers.location; + + if (300 <= status && 400 > status && undefined !== location) { + response.resume(); + + const nextUrl: string = new URL(location, url).toString(); + downloadFile(nextUrl, outputPath, redirectCount + 1).then(resolve, reject); + return; + } + + if (200 > status || 300 <= status) { + response.resume(); + reject(new Error(`Download failed with HTTP ${status}: ${urlText}`)); + return; + } + + writeResponseToFile(response, tmpPath, outputPath, resolve, reject); + } + + const client: typeof http | typeof https = "https:" === url.protocol ? https : http; + const request: http.ClientRequest = client.get(url, onResponse); + + /** + * onRequestError removes the temporary file because a failed request may + * leave a partial stream behind. + */ + request.on("error", function onRequestError(error: Error): void { + rmSync(tmpPath, { force: true }); + reject(error); + }); + + /** + * onRequestTimeout aborts stuck transfers. Large ISO downloads should be + * retried by the caller instead of hanging the entire CI job indefinitely. + */ + request.setTimeout(requestTimeoutMilliseconds, function onRequestTimeout(): void { + request.destroy(new Error(`Download timed out after 300 seconds: ${urlText}`)); + }); + } + + await new Promise(downloadFilePromise); +} + +/** + * writeResponseToFile finishes the stream-to-file part of a verified download. + * Keeping this separate from HTTP validation makes partial-file handling easier + * to audit. + */ +function writeResponseToFile( + response: http.IncomingMessage, + tmpPath: string, + outputPath: string, + resolve: () => void, + reject: (reason?: unknown) => void +): void { + const file = createWriteStream(tmpPath); + response.pipe(file); + + /** + * onFileFinish closes the descriptor before rename. Windows can reject a + * rename while the stream still owns the file handle. + */ + file.on("finish", function onFileFinish(): void { + /** + * onFileClosed publishes the completed temporary file only after Windows has + * released the stream handle. + */ + function onFileClosed(): void { + renameSync(tmpPath, outputPath); + resolve(); + } + + file.close(onFileClosed); + }); + + /** + * onFileError removes the temporary file so the next retry starts from a + * clean path. + */ + file.on("error", function onFileError(error: Error): void { + rmSync(tmpPath, { force: true }); + reject(error); + }); +} + +/** + * sleep creates an explicit async delay for retry backoff. A named helper keeps + * retry code readable without hiding the fact that the action is waiting. + */ +function sleep(milliseconds: number): Promise { + /** + * sleepPromise is intentionally tiny: setTimeout is callback-based, while the + * retry loop reads clearly with await. + */ + function sleepPromise(resolve: () => void): void { + setTimeout(resolve, milliseconds); + } + + return new Promise(sleepPromise); +} + +/** + * formatError extracts the useful message from an unknown thrown value. Native + * APIs and third-party packages do not always throw Error instances. + */ +function formatError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return String(error); +} + +/** + * errorFromUnknown converts the last failed attempt into an Error object. This + * keeps catch sites simple while preserving the original message when possible. + */ +function errorFromUnknown(error: unknown): Error { + if (error instanceof Error) { + return error; + } + + return new Error(String(error)); +} diff --git a/src/files.ts b/src/files.ts new file mode 100644 index 0000000..c84658d --- /dev/null +++ b/src/files.ts @@ -0,0 +1,86 @@ +import { existsSync, readdirSync, statSync } from "node:fs"; +import * as path from "node:path"; + +import { fullPath } from "./paths.js"; + +interface DirectoryVisit { + dir: string; + depth: number; +} + +/** + * listFilesUnder performs a bounded depth-first file scan. The action needs + * this for cache and SDK discovery where directory names vary between WDK + * installers, but a maximum depth prevents accidental scans of huge disks. + */ +export function listFilesUnder( + root: string, + predicate: (filePath: string) => boolean, + maxDepth: number = 10 +): string[] { + const resolvedRoot: string = fullPath(root); + + if ("" === resolvedRoot || false === existsSync(resolvedRoot)) { + return []; + } + + const result: string[] = []; + const stack: DirectoryVisit[] = [{ dir: resolvedRoot, depth: 0 }]; + + while (0 < stack.length) { + const current: DirectoryVisit | undefined = stack.pop(); + + if (undefined === current) { + continue; + } + + // Directory reads may fail inside restored caches; skipping unreadable paths + // lets discovery continue without turning one bad directory into an action failure. + const entries: string[] = readDirectoryEntries(current.dir); + + for (const entry of entries) { + const entryPath: string = path.join(current.dir, entry); + const stats = readStats(entryPath); + + if (undefined === stats) { + continue; + } + + if (true === stats.isDirectory()) { + if (current.depth < maxDepth) { + stack.push({ dir: entryPath, depth: current.depth + 1 }); + } + } else if (true === predicate(entryPath)) { + result.push(entryPath); + } + } + } + + return result; +} + +/** + * readDirectoryEntries isolates filesystem errors from the traversal loop. + * Caches can contain partially written extraction output after a cancelled job, + * and discovery should keep looking for a valid WDK tree elsewhere. + */ +function readDirectoryEntries(directory: string): string[] { + try { + return readdirSync(directory); + } catch { + return []; + } +} + +/** + * readStats wraps statSync so callers can treat inaccessible entries as absent. + * This keeps discovery deterministic on Windows runners with inconsistent ACLs + * or antivirus races during extraction. + */ +function readStats(filePath: string): ReturnType | undefined { + try { + return statSync(filePath); + } catch { + return undefined; + } +} diff --git a/src/iso.ts b/src/iso.ts new file mode 100644 index 0000000..cb48051 --- /dev/null +++ b/src/iso.ts @@ -0,0 +1,177 @@ +import * as core from "@actions/core"; +import { existsSync, mkdirSync, readdirSync } from "node:fs"; +import * as path from "node:path"; + +import { actionRoot } from "./paths.js"; +import { runProcess } from "./process.js"; + +/** + * mountIso mounts an ISO through the small PowerShell helper. Mount-DiskImage is + * Windows-native, so keeping it in PowerShell avoids reimplementing platform + * behavior in TypeScript. + */ +export async function mountIso(isoPath: string): Promise { + const script: string = path.join(actionRoot(), "scripts", "mount-iso.ps1"); + const output: string = await runProcess("powershell.exe", [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + script, + "-ImagePath", + isoPath + ], { silent: true }); + + const drive: string = lastNonEmptyLine(output); + + if ("" === drive) { + throw new Error("Mount-DiskImage did not return a drive letter."); + } + + return drive.replace(":", ""); +} + +/** + * dismountIso always calls the matching PowerShell helper. The helper tolerates + * already-dismounted images so callers can safely use it in finally blocks. + */ +export async function dismountIso(isoPath: string): Promise { + const script: string = path.join(actionRoot(), "scripts", "dismount-iso.ps1"); + + await runProcess("powershell.exe", [ + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + script, + "-ImagePath", + isoPath + ], { silent: true }); +} + +/** + * extractMsi performs an administrative MSI extraction and reports whether it + * succeeded. Debugging Tools packages vary between ISO layouts, so callers can + * try several packages without failing the whole action on the first mismatch. + */ +export async function extractMsi(msiPath: string, targetRoot: string, logRoot: string): Promise { + const baseName: string = path.basename(msiPath, path.extname(msiPath)); + const logPath: string = path.join(logRoot, `${baseName}.log`); + + try { + core.info(`Extracting ${path.basename(msiPath)} to ${targetRoot}`); + await runMsiExtraction(msiPath, targetRoot, logPath); + + return true; + } catch (error) { + core.info(`Skipping ${path.basename(msiPath)}: ${formatError(error)}`); + + return false; + } +} + +/** + * installWdk7FromIso extracts all WDK MSI packages from the mounted ISO. The + * WDK installer is represented as a set of MSI packages, so every package under + * the WDK media directory must be extracted into the same target tree. + */ +export async function installWdk7FromIso(isoPath: string, targetRoot: string): Promise { + core.info(`Mounting WDK7 ISO: ${isoPath}`); + + const drive: string = await mountIso(isoPath); + + try { + const mediaRoot: string = `${drive}:\\WDK`; + + if (false === existsSync(mediaRoot)) { + throw new Error(`Mounted ISO does not contain a WDK directory: ${mediaRoot}`); + } + + mkdirSync(targetRoot, { recursive: true }); + + const logRoot: string = path.join(targetRoot, "_install_logs"); + mkdirSync(logRoot, { recursive: true }); + + const msiFiles: string[] = listWdkMsiFiles(mediaRoot); + + if (0 === msiFiles.length) { + throw new Error(`No WDK MSI packages found under '${mediaRoot}'.`); + } + + for (const msiPath of msiFiles) { + const baseName: string = path.basename(msiPath, path.extname(msiPath)); + const logPath: string = path.join(logRoot, `${baseName}.log`); + + core.info(`Extracting ${path.basename(msiPath)}`); + await runMsiExtraction(msiPath, targetRoot, logPath); + } + } finally { + await dismountIso(isoPath); + } +} + +/** + * runMsiExtraction keeps the exact msiexec command in one place. Administrative + * extraction is used because CI runners need files on disk, not a registered + * system-wide WDK installation. + */ +async function runMsiExtraction(msiPath: string, targetRoot: string, logPath: string): Promise { + await runProcess("msiexec.exe", [ + "/a", + msiPath, + "/qn", + "/norestart", + `TARGETDIR=${targetRoot}`, + "/l*v", + logPath + ]); +} + +/** + * listWdkMsiFiles returns the WDK media packages in deterministic directory + * order. The ISO contains only installer payloads in this directory, so a flat + * scan is clearer than a recursive search. + */ +function listWdkMsiFiles(mediaRoot: string): string[] { + const result: string[] = []; + const entries: string[] = readdirSync(mediaRoot); + + for (const entry of entries) { + if (true === entry.toLowerCase().endsWith(".msi")) { + result.push(path.join(mediaRoot, entry)); + } + } + + return result; +} + +/** + * lastNonEmptyLine parses helper output without relying on PowerShell formatting + * quirks. The mount helper writes the drive letter as the last meaningful line. + */ +function lastNonEmptyLine(output: string): string { + let result: string = ""; + const lines: string[] = output.split(/\r?\n/); + + for (const line of lines) { + const trimmed: string = line.trim(); + + if ("" !== trimmed) { + result = trimmed; + } + } + + return result; +} + +/** + * formatError extracts a readable message from unknown catch values. Keeping + * this local avoids leaking error-formatting policy into the ISO API. + */ +function formatError(error: unknown): string { + if (error instanceof Error) { + return error.message; + } + + return String(error); +} diff --git a/src/lists.ts b/src/lists.ts new file mode 100644 index 0000000..88128a1 --- /dev/null +++ b/src/lists.ts @@ -0,0 +1,21 @@ +/** + * uniqueStrings preserves first-seen order while removing case-insensitive + * duplicates. URL lists and Windows paths both benefit from stable ordering, + * because the first value is usually the most intentional one. + */ +export function uniqueStrings(values: string[]): string[] { + const seen: Set = new Set(); + const result: string[] = []; + + // A manual loop keeps the order and comparison rule visible to future edits. + for (const value of values) { + const key: string = value.toLowerCase(); + + if (false === seen.has(key)) { + seen.add(key); + result.push(value); + } + } + + return result; +} diff --git a/src/main.ts b/src/main.ts index 150930a..ad11811 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,978 +1,203 @@ -import * as core from "@actions/core"; -import * as actionsCache from "@actions/cache"; -import { createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs"; -import * as http from "node:http"; -import * as https from "node:https"; -import * as os from "node:os"; +import { mkdirSync } from "node:fs"; import * as path from "node:path"; -import { spawn } from "node:child_process"; -import { fileURLToPath } from "node:url"; - -const defaultDownloadUrls = [ - "https://download.microsoft.com/download/4/A/2/4A25C7D5-EFBE-4182-B6A9-AE6850409A78/GRMWDK_EN_7600_1.ISO" -]; -const cmakeGenerator = "NMake Makefiles"; -const wdkOnlyCacheKey = "wdk-7600.16385.1"; -const debuggerCacheKey = "wdk-7600.16385.1-debugger"; -const downloadRetries = 3; - -interface Candidate { - root: string; - source: string; -} - -interface Inputs { - root: string; - downloadUrls: string[]; - debugger: boolean; -} - -interface DebuggersSdk { - root: string; - includeDir: string; - libI386: string; - libAmd64: string; - binX86: string; - binX64: string; -} - -function readInputs(): Inputs { - const downloadUrls = splitDownloadUrls(core.getInput("download-url")); - - return { - root: core.getInput("root"), - downloadUrls: uniqueStrings([...downloadUrls, ...defaultDownloadUrls]), - debugger: readBooleanInput("debugger", false) - }; -} - -function splitDownloadUrls(value: string): string[] { - return value - .split(/[\r\n,;]+/) - .map(item => item.trim()) - .filter(Boolean); -} - -function uniqueStrings(values: string[]): string[] { - const seen = new Set(); - const result: string[] = []; - - for (const value of values) { - const key = value.toLowerCase(); - if (!seen.has(key)) { - seen.add(key); - result.push(value); - } - } - - return result; -} - -function readBooleanInput(name: string, defaultValue: boolean): boolean { - const value = core.getInput(name).trim().toLowerCase(); - if (!value) { - return defaultValue; - } - if (value === "true") { - return true; - } - if (value === "false") { - return false; - } - throw new Error(`Input '${name}' must be true or false.`); -} - -function actionRoot(): string { - return path.dirname(path.dirname(fileURLToPath(import.meta.url))); -} - -function cmakeModuleDir(): string { - return path.join(actionRoot(), "cmake"); -} - -function toolchainFile(): string { - return path.join(cmakeModuleDir(), "wdk7.cmake"); -} - -function ddkbuildCmd(): string { - return path.join(actionRoot(), "ddkbuild.cmd"); -} - -function publishStaticOutputs(): void { - core.setOutput("cmake-module-dir", cmakeModuleDir()); - core.setOutput("toolchain-file", toolchainFile()); - core.setOutput("ddkbuild-cmd", ddkbuildCmd()); - core.setOutput("cmake-generator", cmakeGenerator); -} - -function expandEnvironment(value: string): string { - return value.replace(/%([^%]+)%/g, (_match, name: string) => process.env[name] ?? ""); -} -function fullPath(value: string): string { - if (!value.trim()) { - return ""; +import { CacheSession } from "./cache.js"; +import { findDbgEngSdk, prepareDebuggersSdk, prepareOptionalDebuggersSdk } from "./debuggers.js"; +import { ensureWdk7Iso } from "./download.js"; +import { installWdk7FromIso } from "./iso.js"; +import { defaultCacheRoot } from "./paths.js"; +import { publishNotFound, publishStaticOutputs, publishWdk7 } from "./outputs.js"; +import { cacheKeyForDebugger, readInputs, restoreKeysForDebugger } from "./settings.js"; +import type { ActionInputs, DebuggersSdk, PreparedDebuggers, WdkRoot } from "./types.js"; +import { findCachedWdk7Root, findWdk7Root, findWdk7RootUnder, isWdk7Root } from "./wdk.js"; + +/** + * run is the action entry point. It keeps the setup flow readable by delegating + * discovery, cache, download, extraction, debugger SDK, and output concerns to + * focused modules. + */ +async function run(): Promise { + if ("win32" !== process.platform) { + throw new Error("wdk7 only runs on Windows."); } - return path.resolve(expandEnvironment(value)); -} -function targetBins(root: string): string[] { - return [ - path.join(root, "bin", "x86", "x86"), - path.join(root, "bin", "x86", "amd64") - ]; -} - -function hostBin(root: string): string { - return path.join(root, "bin", "x86"); -} + const inputs: ActionInputs = readInputs(); + const cacheRoot: string = defaultCacheRoot(); + const cacheKey: string = cacheKeyForDebugger(inputs.debugger); + const restoreKeys: string[] = restoreKeysForDebugger(inputs.debugger); + const cache: CacheSession = new CacheSession(cacheRoot, cacheKey, restoreKeys); -function isFileOrDirectoryPresent(candidatePath: string): boolean { - return existsSync(candidatePath); -} + // The cache directory must exist before either @actions/cache or local ISO + // extraction can use it. + mkdirSync(cacheRoot, { recursive: true }); -function isWdk7Root(root: string): boolean { - if (!root.trim()) { - return false; - } + publishStaticOutputs(); - const resolved = fullPath(root); - const required = [ - path.join(resolved, "bin", "setenv.bat"), - path.join(resolved, "inc", "api"), - path.join(resolved, "inc", "ddk"), - path.join(hostBin(resolved), "nmake.exe"), - path.join(hostBin(resolved), "rc.exe"), - ...targetBins(resolved).flatMap(bin => [ - path.join(bin, "cl.exe"), - path.join(bin, "link.exe") - ]) - ]; - - return required.every(isFileOrDirectoryPresent); -} + const installed: WdkRoot | undefined = findWdk7Root(inputs.root, cacheRoot, false); -function defaultCacheRoot(): string { - if (process.env.RUNNER_TOOL_CACHE) { - return path.join(process.env.RUNNER_TOOL_CACHE, "wdk7"); - } - if (process.env.LOCALAPPDATA) { - return path.join(process.env.LOCALAPPDATA, "actions-tool-cache", "wdk7"); - } - return path.join(os.tmpdir(), "actions-tool-cache", "wdk7"); -} + if (undefined !== installed) { + await useInstalledRoot(inputs, installed, cache, cacheRoot); -function addCandidate(candidates: Candidate[], root: string | undefined, source: string): void { - if (!root?.trim()) { return; } - const resolved = fullPath(root); - if (!candidates.some(candidate => candidate.root.toLowerCase() === resolved.toLowerCase())) { - candidates.push({ root: resolved, source }); - } -} - -function findWdk7Root( - requestedRoot: string, - cacheRoot: string, - includeCache: boolean -): Candidate | undefined { - const candidates: Candidate[] = []; - - addCandidate(candidates, requestedRoot, "input"); - addCandidate(candidates, process.env.WDK7_ROOT, "environment"); - addCandidate(candidates, process.env.W7BASE, "environment"); - - if (includeCache) { - addCandidate(candidates, path.join(cacheRoot, "7600.16385.1"), "cache"); - addCandidate(candidates, path.join(cacheRoot, "7600.16385.win7_wdk.100208-1538"), "cache"); - addCandidate(candidates, path.join(cacheRoot, "wdk7", "7600.16385.1"), "cache"); - addCandidate(candidates, path.join(cacheRoot, "wdk7", "7600.16385.win7_wdk.100208-1538"), "cache"); - } - - addCandidate(candidates, "C:\\WinDDK\\7600.16385.1", "default"); - addCandidate(candidates, "C:\\WinDDK\\7600.16385.win7_wdk.100208-1538", "default"); - - return candidates.find(candidate => isWdk7Root(candidate.root)); -} - -function findWdk7RootUnder(basePath: string): string | undefined { - const resolvedBase = fullPath(basePath); - if (!resolvedBase || !existsSync(resolvedBase)) { - return undefined; - } + await cache.restoreOnce(); - if (isWdk7Root(resolvedBase)) { - return resolvedBase; - } + let found: WdkRoot | undefined = findWdk7Root(inputs.root, cacheRoot, true); - const stack = [resolvedBase]; - while (stack.length > 0) { - const current = stack.pop(); - if (!current) { - continue; - } - - let entries: string[]; - try { - entries = readdirSync(current); - } catch { - continue; - } - - for (const entry of entries) { - const entryPath = path.join(current, entry); - let stats; - try { - stats = statSync(entryPath); - } catch { - continue; - } - - if (stats.isDirectory()) { - stack.push(entryPath); - } else if (entry.toLowerCase() === "setenv.bat") { - const root = path.dirname(path.dirname(entryPath)); - if (isWdk7Root(root)) { - return fullPath(root); - } - } - } + // Older cache layouts can place WDK7 below an extra directory level, so the + // broad scan runs only after the clear candidate list fails. + if (undefined === found) { + found = findCachedWdk7Root(cacheRoot); } - return undefined; -} + if (undefined !== found) { + await useCachedRoot(inputs, found, cache, cacheRoot); -function findCachedWdk7Root(cacheRoot: string): Candidate | undefined { - const root = findWdk7RootUnder(cacheRoot); - return root ? { root, source: "cache" } : undefined; -} - -function hasDbgEngInclude(includeDir: string): boolean { - return existsSync(path.join(includeDir, "DbgEng.h")); -} - -function hasDbgEngLibraries(libraryDir: string): boolean { - return existsSync(path.join(libraryDir, "dbgeng.lib")) && - existsSync(path.join(libraryDir, "dbghelp.lib")); -} - -function debuggerBin(root: string, arch: "x86" | "x64"): string { - const candidates = [ - path.join(root, arch), - path.join(root, "Debuggers", arch), - path.join(root, "Debugging Tools for Windows", arch), - path.join(root, `Debugging Tools for Windows (${arch})`) - ]; - - return candidates.find(candidate => existsSync(candidate)) ?? ""; -} - -function createDebuggersSdk( - root: string, - includeDir: string, - libI386: string, - libAmd64: string -): DebuggersSdk | undefined { - if (!hasDbgEngInclude(includeDir) || !hasDbgEngLibraries(libI386) || !hasDbgEngLibraries(libAmd64)) { - return undefined; - } - - const resolvedRoot = fullPath(root); - return { - root: resolvedRoot, - includeDir: fullPath(includeDir), - libI386: fullPath(libI386), - libAmd64: fullPath(libAmd64), - binX86: debuggerBin(resolvedRoot, "x86"), - binX64: debuggerBin(resolvedRoot, "x64") - }; -} - -function findDbgEngSdkInKnownLayouts(root: string): DebuggersSdk | undefined { - const resolved = fullPath(root); - if (!resolved || !existsSync(resolved)) { - return undefined; - } - - const layouts = [ - { - root: path.join(resolved, "Debuggers"), - includeDir: path.join(resolved, "Debuggers", "sdk", "inc"), - libI386: path.join(resolved, "Debuggers", "sdk", "lib", "i386"), - libAmd64: path.join(resolved, "Debuggers", "sdk", "lib", "amd64") - }, - { - root: resolved, - includeDir: path.join(resolved, "sdk", "inc"), - libI386: path.join(resolved, "sdk", "lib", "i386"), - libAmd64: path.join(resolved, "sdk", "lib", "amd64") - }, - { - root: path.join(resolved, "Debuggers"), - includeDir: path.join(resolved, "Debuggers", "inc"), - libI386: path.join(resolved, "Debuggers", "lib", "x86"), - libAmd64: path.join(resolved, "Debuggers", "lib", "x64") - }, - { - root: resolved, - includeDir: path.join(resolved, "inc"), - libI386: path.join(resolved, "lib", "x86"), - libAmd64: path.join(resolved, "lib", "x64") - }, - { - root: resolved, - includeDir: path.join(resolved, "Include"), - libI386: path.join(resolved, "Lib"), - libAmd64: path.join(resolved, "Lib", "x64") - } - ]; - - for (const layout of layouts) { - const sdk = createDebuggersSdk(layout.root, layout.includeDir, layout.libI386, layout.libAmd64); - if (sdk) { - return sdk; - } - } - - return undefined; -} - -function listFilesUnder(root: string, predicate: (filePath: string) => boolean, maxDepth = 10): string[] { - const resolved = fullPath(root); - if (!resolved || !existsSync(resolved)) { - return []; - } - - const result: string[] = []; - const stack: Array<{ dir: string; depth: number }> = [{ dir: resolved, depth: 0 }]; - - while (stack.length > 0) { - const current = stack.pop(); - if (!current) { - continue; - } - - let entries: string[]; - try { - entries = readdirSync(current.dir); - } catch { - continue; - } - - for (const entry of entries) { - const entryPath = path.join(current.dir, entry); - let stats; - try { - stats = statSync(entryPath); - } catch { - continue; - } - - if (stats.isDirectory()) { - if (current.depth < maxDepth) { - stack.push({ dir: entryPath, depth: current.depth + 1 }); - } - } else if (predicate(entryPath)) { - result.push(entryPath); - } - } - } - - return result; -} - -function findDbgEngSdkUnder(root: string): DebuggersSdk | undefined { - const known = findDbgEngSdkInKnownLayouts(root); - if (known) { - return known; - } - - const includeFiles = listFilesUnder(root, filePath => path.basename(filePath).toLowerCase() === "dbgeng.h"); - if (includeFiles.length === 0) { - return undefined; + return; } - const libDirs = uniqueStrings( - listFilesUnder(root, filePath => path.basename(filePath).toLowerCase() === "dbgeng.lib") - .map(filePath => path.dirname(filePath)) - .filter(hasDbgEngLibraries) + await downloadAndUseRoot(inputs, cache, cacheRoot); +} + +/** + * useInstalledRoot handles explicit, environment, and default local WDK roots. + * These roots are not saved back into the WDK cache, but newly extracted + * Debugging Tools cache files should still be preserved. + */ +async function useInstalledRoot( + inputs: ActionInputs, + installed: WdkRoot, + cache: CacheSession, + cacheRoot: string +): Promise { + const prepared: PreparedDebuggers = await prepareInstalledDebuggers(inputs, installed.root, cache, cacheRoot); + + await cache.saveWhenChanged(prepared.cacheChanged); + + publishWdk7(installed.root, installed.source, cache.hasRestored(), prepared.sdk); +} + +/** + * useCachedRoot handles roots found after cache restore or local cache probing. + * Saving under the requested key preserves local cache discoveries and upgrades + * WDK-only restores to debugger-enabled cache entries when needed. + */ +async function useCachedRoot( + inputs: ActionInputs, + found: WdkRoot, + cache: CacheSession, + cacheRoot: string +): Promise { + const prepared: PreparedDebuggers = await prepareOptionalDebuggersSdk( + inputs.debugger, + found.root, + cacheRoot, + inputs.downloadUrls ); - if (libDirs.length === 0) { - return undefined; - } - - const amd64Lib = libDirs.find(dir => /[\\\/](amd64|x64)([\\\/]|$)/i.test(dir)); - const i386Lib = - libDirs.find(dir => /[\\\/](i386|x86)([\\\/]|$)/i.test(dir) && !/[\\\/](amd64|x64)([\\\/]|$)/i.test(dir)) ?? - libDirs.find(dir => !/[\\\/](amd64|x64)([\\\/]|$)/i.test(dir)); - - if (!i386Lib || !amd64Lib) { - return undefined; - } - - for (const includeFile of includeFiles) { - const includeDir = path.dirname(includeFile); - let debuggerRoot = path.dirname(includeDir); - if (path.basename(debuggerRoot).toLowerCase() === "sdk") { - debuggerRoot = path.dirname(debuggerRoot); - } - - const sdk = createDebuggersSdk(debuggerRoot, includeDir, i386Lib, amd64Lib); - if (sdk) { - return sdk; - } - } + await cache.saveIfDifferentKey(); - return undefined; -} - -function findDbgEngSdk(wdkRoot: string, cacheRoot: string): DebuggersSdk | undefined { - return findDbgEngSdkUnder(wdkRoot) ?? findDbgEngSdkUnder(cacheRoot); -} - -function runProcess(command: string, args: string[], options?: { cwd?: string; silent?: boolean }): Promise { - return new Promise((resolve, reject) => { - core.debug(`Running: ${command} ${args.join(" ")}`); - - const child = spawn(command, args, { - cwd: options?.cwd, - windowsHide: true - }); - - let stdout = ""; - let stderr = ""; - - child.stdout.on("data", chunk => { - const text = chunk.toString(); - stdout += text; - if (!options?.silent) { - process.stdout.write(text); - } - }); - - child.stderr.on("data", chunk => { - const text = chunk.toString(); - stderr += text; - if (!options?.silent) { - process.stderr.write(text); - } - }); - - child.on("error", reject); - child.on("close", code => { - if (code === 0) { - resolve(stdout.trim()); - } else { - reject(new Error(`${command} failed with exit code ${code}. ${stderr.trim()}`)); - } - }); - }); + publishWdk7( + found.root, + found.source, + "cache" === found.source || true === cache.hasRestored(), + prepared.sdk + ); } -function sleep(milliseconds: number): Promise { - return new Promise(resolve => setTimeout(resolve, milliseconds)); -} +/** + * downloadAndUseRoot installs WDK7 from the ISO when no existing tree is usable. + * The final validation searches both the target cache directory and C:\WinDDK + * because MSI extraction can choose either layout depending on package metadata. + */ +async function downloadAndUseRoot(inputs: ActionInputs, cache: CacheSession, cacheRoot: string): Promise { + if (0 === inputs.downloadUrls.length) { + publishNotFound("WDK7 was not found and no download URLs are configured."); -async function downloadFile(urlText: string, outputPath: string, redirectCount = 0): Promise { - if (redirectCount > 8) { - throw new Error(`Too many redirects while downloading '${urlText}'.`); + return; } - mkdirSync(path.dirname(outputPath), { recursive: true }); - const url = new URL(urlText); - const client = url.protocol === "https:" ? https : http; - const tmpPath = `${outputPath}.tmp`; - - await new Promise((resolve, reject) => { - const request = client.get(url, response => { - const status = response.statusCode ?? 0; - const location = response.headers.location; - - if (status >= 300 && status < 400 && location) { - response.resume(); - const nextUrl = new URL(location, url).toString(); - downloadFile(nextUrl, outputPath, redirectCount + 1).then(resolve, reject); - return; - } - - if (status < 200 || status >= 300) { - response.resume(); - reject(new Error(`Download failed with HTTP ${status}: ${urlText}`)); - return; - } - - const file = createWriteStream(tmpPath); - response.pipe(file); - file.on("finish", () => { - file.close(() => { - renameSync(tmpPath, outputPath); - resolve(); - }); - }); - file.on("error", error => { - rmSync(tmpPath, { force: true }); - reject(error); - }); - }); - - request.on("error", error => { - rmSync(tmpPath, { force: true }); - reject(error); - }); - - request.setTimeout(300000, () => { - request.destroy(new Error(`Download timed out after 300 seconds: ${urlText}`)); - }); - }); -} - -async function downloadFileWithRetries(urlText: string, outputPath: string, attempts: number): Promise { - let lastError: unknown; - - for (let attempt = 1; attempt <= attempts; attempt += 1) { - try { - await downloadFile(urlText, outputPath); - return; - } catch (error) { - lastError = error; - rmSync(outputPath, { force: true }); - - if (attempt >= attempts) { - break; - } - - const delay = Math.min(30000, 2000 * attempt); - core.warning( - `WDK7 ISO download attempt ${attempt}/${attempts} failed: ${ - error instanceof Error ? error.message : String(error) - }. Retrying in ${delay / 1000}s.` - ); - await sleep(delay); - } - } + const isoPath: string = await ensureWdk7Iso(cacheRoot, inputs.downloadUrls); + const targetRoot: string = path.join(cacheRoot, "7600.16385.1"); - throw lastError instanceof Error ? lastError : new Error(String(lastError)); -} - -async function downloadFileFromUrlsWithRetries( - urls: string[], - outputPath: string, - attempts: number -): Promise { - let lastError: unknown; - - for (let index = 0; index < urls.length; index += 1) { - const url = urls[index]; - try { - core.info(`Downloading WDK7 ISO from source ${index + 1}/${urls.length}: ${url}`); - await downloadFileWithRetries(url, outputPath, attempts); - return url; - } catch (error) { - lastError = error; - rmSync(outputPath, { force: true }); - - if (index + 1 < urls.length) { - core.warning( - `WDK7 ISO source ${index + 1}/${urls.length} failed: ${ - error instanceof Error ? error.message : String(error) - }. Trying next source.` - ); - } - } + if (false === isWdk7Root(targetRoot)) { + await installWdk7FromIso(isoPath, targetRoot); } - throw lastError instanceof Error ? lastError : new Error(String(lastError)); -} - -async function ensureWdk7Iso(cacheRoot: string, urls: string[]): Promise { - const isoPath = path.join(cacheRoot, "GRMWDK_EN_7600_1.ISO"); + let resolvedRoot: string | undefined = findWdk7RootUnder(targetRoot); - if (existsSync(isoPath)) { - core.info(`Using cached WDK7 ISO: ${isoPath}`); - return isoPath; + // Administrative MSI extraction may use C:\WinDDK metadata even when the + // action requested a cache target directory, so both locations are validated. + if (undefined === resolvedRoot) { + resolvedRoot = findWdk7RootUnder("C:\\WinDDK"); } - const downloadedUrl = await downloadFileFromUrlsWithRetries(urls, isoPath, downloadRetries); - core.info(`Downloaded WDK7 ISO from: ${downloadedUrl}`); - return isoPath; -} - -async function mountIso(isoPath: string): Promise { - const script = path.join(actionRoot(), "scripts", "mount-iso.ps1"); - const output = await runProcess("powershell.exe", [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - script, - "-ImagePath", - isoPath - ], { silent: true }); - - const drive = output.split(/\r?\n/).map(line => line.trim()).filter(Boolean).pop(); - if (!drive) { - throw new Error("Mount-DiskImage did not return a drive letter."); + if (undefined === resolvedRoot) { + throw new Error("WDK7 extraction completed, but no valid WDK7 root was found."); } - return drive.replace(":", ""); -} -async function dismountIso(isoPath: string): Promise { - const script = path.join(actionRoot(), "scripts", "dismount-iso.ps1"); - await runProcess("powershell.exe", [ - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - script, - "-ImagePath", - isoPath - ], { silent: true }); -} - -function findDebuggersMsiFiles(mediaRoot: string): string[] { - return listFilesUnder(mediaRoot, filePath => { - const lower = filePath.toLowerCase(); - const baseName = path.basename(lower); - return baseName.endsWith(".msi") && - (baseName.startsWith("dbg") || - lower.includes("debuggingtools") || - lower.includes("debuggers")); - }); -} + const prepared: PreparedDebuggers = await prepareOptionalDebuggersSdk( + inputs.debugger, + resolvedRoot, + cacheRoot, + inputs.downloadUrls + ); -async function extractMsi(msiPath: string, targetRoot: string, logRoot: string): Promise { - const baseName = path.basename(msiPath, path.extname(msiPath)); - const logPath = path.join(logRoot, `${baseName}.log`); - - try { - core.info(`Extracting ${path.basename(msiPath)} to ${targetRoot}`); - await runProcess("msiexec.exe", [ - "/a", - msiPath, - "/qn", - "/norestart", - `TARGETDIR=${targetRoot}`, - "/l*v", - logPath - ]); - return true; - } catch (error) { - core.info(`Skipping ${path.basename(msiPath)}: ${error instanceof Error ? error.message : String(error)}`); - return false; - } -} + await cache.saveIfDifferentKey(); -async function installDebuggersFromIso(isoPath: string, wdkRoot: string, cacheRoot: string): Promise { - core.info(`Mounting WDK7 ISO for Debugging Tools: ${isoPath}`); - const drive = await mountIso(isoPath); - - try { - const mediaRoot = `${drive}:\\`; - const msiFiles = findDebuggersMsiFiles(mediaRoot); - if (msiFiles.length === 0) { - core.info("No Debugging Tools MSI packages were found in the WDK7 ISO."); - return false; - } - - let changed = false; - const targetRoots = uniqueStrings([ - wdkRoot, - path.join(cacheRoot, "debuggers") - ]); - - for (const targetRoot of targetRoots) { - mkdirSync(targetRoot, { recursive: true }); - const logRoot = path.join(targetRoot, "_debuggers_install_logs"); - mkdirSync(logRoot, { recursive: true }); - - for (const msiPath of msiFiles) { - changed = await extractMsi(msiPath, targetRoot, logRoot) || changed; - } - - if (findDbgEngSdk(wdkRoot, cacheRoot)) { - return changed; - } - } - - return changed; - } finally { - await dismountIso(isoPath); - } + publishWdk7(resolvedRoot, "download", false, prepared.sdk); } -async function prepareDebuggersSdk( +/** + * prepareInstalledDebuggers performs the extra debugger lookup needed for local + * WDK installations. Cache restore is delayed until the selected WDK root has no + * SDK, which avoids unnecessary cache work for plain local installs. + */ +async function prepareInstalledDebuggers( + inputs: ActionInputs, wdkRoot: string, - cacheRoot: string, - downloadUrls: string[] -): Promise<{ sdk?: DebuggersSdk; cacheChanged: boolean }> { - let sdk = findDbgEngSdk(wdkRoot, cacheRoot); - if (sdk) { - return { sdk, cacheChanged: false }; - } - - if (downloadUrls.length === 0) { - core.info("WDK7 Debuggers SDK was not found and no download URLs are configured."); + cache: CacheSession, + cacheRoot: string +): Promise { + if (false === inputs.debugger) { return { cacheChanged: false }; } - const isoPath = await ensureWdk7Iso(cacheRoot, downloadUrls); - const changed = await installDebuggersFromIso(isoPath, wdkRoot, cacheRoot); - sdk = findDbgEngSdk(wdkRoot, cacheRoot); - - if (!sdk) { - core.info("WDK7 Debuggers SDK was not found after Debugging Tools extraction."); - } - - return { sdk, cacheChanged: changed }; -} - -async function installWdk7FromIso(isoPath: string, targetRoot: string): Promise { - core.info(`Mounting WDK7 ISO: ${isoPath}`); - const drive = await mountIso(isoPath); - - try { - const mediaRoot = `${drive}:\\WDK`; - if (!existsSync(mediaRoot)) { - throw new Error(`Mounted ISO does not contain a WDK directory: ${mediaRoot}`); - } - - mkdirSync(targetRoot, { recursive: true }); - const logRoot = path.join(targetRoot, "_install_logs"); - mkdirSync(logRoot, { recursive: true }); - - const msiFiles = readdirSync(mediaRoot) - .filter(entry => entry.toLowerCase().endsWith(".msi")) - .map(entry => path.join(mediaRoot, entry)); - - if (msiFiles.length === 0) { - throw new Error(`No WDK MSI packages found under '${mediaRoot}'.`); - } - - for (const msiPath of msiFiles) { - const baseName = path.basename(msiPath, path.extname(msiPath)); - const logPath = path.join(logRoot, `${baseName}.log`); - core.info(`Extracting ${path.basename(msiPath)}`); - await runProcess("msiexec.exe", [ - "/a", - msiPath, - "/qn", - "/norestart", - `TARGETDIR=${targetRoot}`, - "/l*v", - logPath - ]); - } - } finally { - await dismountIso(isoPath); - } -} - -async function restoreActionCache(cacheRoot: string, cacheKey: string, restoreKeys: string[]): Promise { - if (!actionsCache.isFeatureAvailable()) { - core.info("Actions cache service is not available; using local disk cache only."); - return undefined; - } - - try { - core.info(`Restoring WDK7 cache with key '${cacheKey}'.`); - const hit = await actionsCache.restoreCache([cacheRoot], cacheKey, restoreKeys); - if (hit) { - core.info(`Restored WDK7 cache from key '${hit}'.`); - } else { - core.info("No WDK7 actions/cache entry was restored."); - } - return hit; - } catch (error) { - core.warning(`WDK7 cache restore failed: ${error instanceof Error ? error.message : String(error)}`); - return undefined; - } -} - -async function saveActionCache(cacheRoot: string, cacheKey: string): Promise { - if (!actionsCache.isFeatureAvailable()) { - core.info("Actions cache service is not available; skipping WDK7 cache save."); - return; - } - - try { - core.info(`Saving WDK7 cache with key '${cacheKey}'.`); - await actionsCache.saveCache([cacheRoot], cacheKey); - } catch (error) { - core.warning(`WDK7 cache save skipped: ${error instanceof Error ? error.message : String(error)}`); - } -} + let sdk: DebuggersSdk | undefined = findDbgEngSdk(wdkRoot, cacheRoot); -function publishDebuggersSdk(sdk?: DebuggersSdk): void { - if (!sdk) { - core.setOutput("dbgeng-found", "false"); - core.setOutput("debuggers-root", ""); - core.setOutput("dbgeng-include-dir", ""); - core.setOutput("dbgeng-lib-i386", ""); - core.setOutput("dbgeng-lib-amd64", ""); - core.setOutput("debuggers-bin-x86", ""); - core.setOutput("debuggers-bin-x64", ""); - return; + if (undefined !== sdk) { + return { + sdk: sdk, + cacheChanged: false + }; } - core.exportVariable("WDK7_DEBUGGERS_ROOT", sdk.root); - core.exportVariable("WDK7_DBGENG_INCLUDE_DIR", sdk.includeDir); - core.exportVariable("WDK7_DBGENG_LIB_I386", sdk.libI386); - core.exportVariable("WDK7_DBGENG_LIB_AMD64", sdk.libAmd64); - core.exportVariable("WDK7_DEBUGGERS_BIN_X86", sdk.binX86); - core.exportVariable("WDK7_DEBUGGERS_BIN_X64", sdk.binX64); - - core.setOutput("dbgeng-found", "true"); - core.setOutput("debuggers-root", sdk.root); - core.setOutput("dbgeng-include-dir", sdk.includeDir); - core.setOutput("dbgeng-lib-i386", sdk.libI386); - core.setOutput("dbgeng-lib-amd64", sdk.libAmd64); - core.setOutput("debuggers-bin-x86", sdk.binX86); - core.setOutput("debuggers-bin-x64", sdk.binX64); - - core.info( - `WDK7 Debuggers SDK ready: root='${sdk.root}' include='${sdk.includeDir}' ` + - `lib-i386='${sdk.libI386}' lib-amd64='${sdk.libAmd64}'` - ); -} - -function publishWdk7(root: string, source: string, cacheHit: boolean, sdk?: DebuggersSdk): void { - const resolvedRoot = fullPath(root); - const host = hostBin(resolvedRoot); - - core.exportVariable("WDK7_ROOT", resolvedRoot); - core.exportVariable("W7BASE", resolvedRoot); - core.exportVariable("WDK7_HOST_BIN", host); - core.exportVariable("WDK7_CMAKE_MODULE_DIR", cmakeModuleDir()); - core.exportVariable("WDK7_CMAKE_TOOLCHAIN_FILE", toolchainFile()); - core.exportVariable("WDK7_DDKBUILD_CMD", ddkbuildCmd()); - core.exportVariable("WDK7_CMAKE_GENERATOR", cmakeGenerator); + await cache.restoreOnce(); - core.addPath(host); - - core.setOutput("found", "true"); - core.setOutput("root", resolvedRoot); - core.setOutput("source", source); - core.setOutput("cache-hit", cacheHit ? "true" : "false"); - publishDebuggersSdk(sdk); - - core.info(`WDK7 ready: root='${resolvedRoot}' source='${source}'`); -} - -function publishNotFound(reason: string): void { - core.info(reason); - publishStaticOutputs(); - core.setOutput("found", "false"); - core.setOutput("root", ""); - core.setOutput("source", "none"); - core.setOutput("cache-hit", "false"); - publishDebuggersSdk(undefined); -} + sdk = findDbgEngSdk(wdkRoot, cacheRoot); -async function prepareOptionalDebuggersSdk( - enabled: boolean, - wdkRoot: string, - cacheRoot: string, - downloadUrls: string[] -): Promise<{ sdk?: DebuggersSdk; cacheChanged: boolean }> { - if (!enabled) { - core.info("Debugging Tools SDK was not requested. Set debugger: true to prepare DbgEng headers and libraries."); - return { cacheChanged: false }; + if (undefined !== sdk) { + return { + sdk: sdk, + cacheChanged: false + }; } - return prepareDebuggersSdk(wdkRoot, cacheRoot, downloadUrls); + return prepareDebuggersSdk(wdkRoot, cacheRoot, inputs.downloadUrls); } -async function run(): Promise { - if (process.platform !== "win32") { - throw new Error("wdk7 only runs on Windows."); - } +/** + * onRunError preserves the action's existing graceful-failure behavior. The + * action publishes found=false instead of terminating before downstream steps + * can inspect outputs. + */ +run().catch(function onRunError(error: unknown): void { + if (error instanceof Error) { + publishNotFound(`wdk7 failed: ${error.message}`); - const inputs = readInputs(); - const cacheRoot = defaultCacheRoot(); - const cacheKey = inputs.debugger ? debuggerCacheKey : wdkOnlyCacheKey; - const restoreKeys = inputs.debugger ? [wdkOnlyCacheKey] : []; - mkdirSync(cacheRoot, { recursive: true }); - publishStaticOutputs(); - - let restoredCacheKey: string | undefined; - let cacheRestoreAttempted = false; - const restoreCacheOnce = async (): Promise => { - if (!cacheRestoreAttempted) { - cacheRestoreAttempted = true; - restoredCacheKey = await restoreActionCache(cacheRoot, cacheKey, restoreKeys); - } - }; - - const installed = findWdk7Root(inputs.root, cacheRoot, false); - if (installed) { - let sdk = inputs.debugger ? findDbgEngSdk(installed.root, cacheRoot) : undefined; - let cacheChanged = false; - - if (inputs.debugger && !sdk) { - await restoreCacheOnce(); - sdk = findDbgEngSdk(installed.root, cacheRoot); - } - - if (inputs.debugger && !sdk) { - const prepared = await prepareOptionalDebuggersSdk(true, installed.root, cacheRoot, inputs.downloadUrls); - sdk = prepared.sdk; - cacheChanged = prepared.cacheChanged; - } - - publishWdk7(installed.root, installed.source, Boolean(restoredCacheKey), sdk); - return; - } - - await restoreCacheOnce(); - - const found = findWdk7Root(inputs.root, cacheRoot, true) ?? findCachedWdk7Root(cacheRoot); - if (found) { - const prepared = await prepareOptionalDebuggersSdk(inputs.debugger, found.root, cacheRoot, inputs.downloadUrls); - if (restoredCacheKey !== cacheKey) { - await saveActionCache(cacheRoot, cacheKey); - } - - publishWdk7( - found.root, - found.source, - found.source === "cache" || Boolean(restoredCacheKey), - prepared.sdk - ); return; } - if (inputs.downloadUrls.length === 0) { - publishNotFound("WDK7 was not found and no download URLs are configured."); - return; - } - - const isoPath = await ensureWdk7Iso(cacheRoot, inputs.downloadUrls); - - const targetRoot = path.join(cacheRoot, "7600.16385.1"); - if (!isWdk7Root(targetRoot)) { - await installWdk7FromIso(isoPath, targetRoot); - } - - const resolvedRoot = - findWdk7RootUnder(targetRoot) ?? - findWdk7RootUnder("C:\\WinDDK"); - - if (!resolvedRoot) { - throw new Error("WDK7 extraction completed, but no valid WDK7 root was found."); - } - - const prepared = await prepareOptionalDebuggersSdk(inputs.debugger, resolvedRoot, cacheRoot, inputs.downloadUrls); - - if (restoredCacheKey !== cacheKey) { - await saveActionCache(cacheRoot, cacheKey); - } - - publishWdk7(resolvedRoot, "download", false, prepared.sdk); -} - -run().catch(error => { - publishNotFound(`wdk7 failed: ${error instanceof Error ? error.message : String(error)}`); + publishNotFound(`wdk7 failed: ${String(error)}`); }); diff --git a/src/outputs.ts b/src/outputs.ts new file mode 100644 index 0000000..ca3a482 --- /dev/null +++ b/src/outputs.ts @@ -0,0 +1,105 @@ +import * as core from "@actions/core"; + +import { cmakeGenerator } from "./settings.js"; +import { cmakeModuleDir, ddkbuildCmd, fullPath, hostBin, toolchainFile } from "./paths.js"; +import type { DebuggersSdk } from "./types.js"; + +/** + * publishStaticOutputs exposes action-bundled assets before WDK discovery + * finishes. Even a failed setup can then tell users where the CMake toolchain + * and compatibility wrapper would have been resolved from. + */ +export function publishStaticOutputs(): void { + core.setOutput("cmake-module-dir", cmakeModuleDir()); + core.setOutput("toolchain-file", toolchainFile()); + core.setOutput("ddkbuild-cmd", ddkbuildCmd()); + core.setOutput("cmake-generator", cmakeGenerator); +} + +/** + * publishWdk7 exports a usable WDK root to later workflow steps. Environment + * variables are exported for shell convenience, while outputs preserve stable + * step references for workflow YAML. + */ +export function publishWdk7(root: string, source: string, cacheHit: boolean, sdk?: DebuggersSdk): void { + const resolvedRoot: string = fullPath(root); + const host: string = hostBin(resolvedRoot); + + core.exportVariable("WDK7_ROOT", resolvedRoot); + core.exportVariable("W7BASE", resolvedRoot); + core.exportVariable("WDK7_HOST_BIN", host); + core.exportVariable("WDK7_CMAKE_MODULE_DIR", cmakeModuleDir()); + core.exportVariable("WDK7_CMAKE_TOOLCHAIN_FILE", toolchainFile()); + core.exportVariable("WDK7_DDKBUILD_CMD", ddkbuildCmd()); + core.exportVariable("WDK7_CMAKE_GENERATOR", cmakeGenerator); + + // The host tools are placed on PATH because NMake and rc.exe are used by both + // CMake and legacy build scripts after the action step completes. + core.addPath(host); + + core.setOutput("found", "true"); + core.setOutput("root", resolvedRoot); + core.setOutput("source", source); + core.setOutput("cache-hit", true === cacheHit ? "true" : "false"); + + publishDebuggersSdk(sdk); + + core.info(`WDK7 ready: root='${resolvedRoot}' source='${source}'`); +} + +/** + * publishNotFound records a graceful failure state instead of throwing away all + * outputs. Existing workflows can check found=false and decide whether a WDK7 + * build should be skipped. + */ +export function publishNotFound(reason: string): void { + core.info(reason); + + publishStaticOutputs(); + + core.setOutput("found", "false"); + core.setOutput("root", ""); + core.setOutput("source", "none"); + core.setOutput("cache-hit", "false"); + + publishDebuggersSdk(undefined); +} + +/** + * publishDebuggersSdk exports optional Debugging Tools paths. Empty outputs are + * always written when the SDK is absent so downstream workflow conditions do + * not depend on missing-output behavior. + */ +function publishDebuggersSdk(sdk?: DebuggersSdk): void { + if (undefined === sdk) { + core.setOutput("dbgeng-found", "false"); + core.setOutput("debuggers-root", ""); + core.setOutput("dbgeng-include-dir", ""); + core.setOutput("dbgeng-lib-i386", ""); + core.setOutput("dbgeng-lib-amd64", ""); + core.setOutput("debuggers-bin-x86", ""); + core.setOutput("debuggers-bin-x64", ""); + + return; + } + + core.exportVariable("WDK7_DEBUGGERS_ROOT", sdk.root); + core.exportVariable("WDK7_DBGENG_INCLUDE_DIR", sdk.includeDir); + core.exportVariable("WDK7_DBGENG_LIB_I386", sdk.libI386); + core.exportVariable("WDK7_DBGENG_LIB_AMD64", sdk.libAmd64); + core.exportVariable("WDK7_DEBUGGERS_BIN_X86", sdk.binX86); + core.exportVariable("WDK7_DEBUGGERS_BIN_X64", sdk.binX64); + + core.setOutput("dbgeng-found", "true"); + core.setOutput("debuggers-root", sdk.root); + core.setOutput("dbgeng-include-dir", sdk.includeDir); + core.setOutput("dbgeng-lib-i386", sdk.libI386); + core.setOutput("dbgeng-lib-amd64", sdk.libAmd64); + core.setOutput("debuggers-bin-x86", sdk.binX86); + core.setOutput("debuggers-bin-x64", sdk.binX64); + + core.info( + `WDK7 Debuggers SDK ready: root='${sdk.root}' include='${sdk.includeDir}' ` + + `lib-i386='${sdk.libI386}' lib-amd64='${sdk.libAmd64}'` + ); +} diff --git a/src/paths.ts b/src/paths.ts new file mode 100644 index 0000000..1cb324c --- /dev/null +++ b/src/paths.ts @@ -0,0 +1,121 @@ +import * as os from "node:os"; +import * as path from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * actionRoot returns the repository root at runtime. The bundled action entry + * lives under dist, so walking up one level keeps bundled assets such as CMake + * files and ddkbuild.cmd addressable after packaging. + */ +export function actionRoot(): string { + const bundledEntry: string = fileURLToPath(import.meta.url); + const distDirectory: string = path.dirname(bundledEntry); + + return path.dirname(distDirectory); +} + +/** + * cmakeModuleDir points to the bundled CMake support directory. Keeping this as + * a helper prevents output publishing and ISO scripts from duplicating layout + * assumptions. + */ +export function cmakeModuleDir(): string { + return path.join(actionRoot(), "cmake"); +} + +/** + * toolchainFile returns the WDK7 CMake toolchain exposed to downstream builds. + * It is derived from the module directory so a future move only changes one + * path helper. + */ +export function toolchainFile(): string { + return path.join(cmakeModuleDir(), "wdk7.cmake"); +} + +/** + * ddkbuildCmd returns the bundled compatibility wrapper for legacy projects. + * The action exports this path so workflow authors do not need to hardcode the + * repository layout. + */ +export function ddkbuildCmd(): string { + return path.join(actionRoot(), "ddkbuild.cmd"); +} + +/** + * expandEnvironment resolves Windows-style %NAME% references in user-provided + * paths. WDK roots are often configured through environment variables on + * self-hosted runners, so this keeps those inputs usable. + */ +export function expandEnvironment(value: string): string { + // Windows users commonly pass roots such as %WDK7_ROOT%, so expansion happens + // before path normalization instead of forcing callers to resolve variables. + return value.replace(/%([^%]+)%/g, replaceEnvironmentToken); +} + +/** + * fullPath normalizes an input path after environment expansion. Empty values + * stay empty so callers can distinguish "not configured" from a valid current + * directory path. + */ +export function fullPath(value: string): string { + if ("" === value.trim()) { + return ""; + } + + return path.resolve(expandEnvironment(value)); +} + +/** + * targetBins lists the compiler directories that must exist for both WDK7 + * target architectures. A root is only useful to the action when both x86 and + * amd64 build tools are present. + */ +export function targetBins(root: string): string[] { + return [ + path.join(root, "bin", "x86", "x86"), + path.join(root, "bin", "x86", "amd64") + ]; +} + +/** + * hostBin returns the WDK host-tool directory used by NMake and resource tools. + * WDK7 uses x86 host tools even when the target architecture is amd64. + */ +export function hostBin(root: string): string { + return path.join(root, "bin", "x86"); +} + +/** + * defaultCacheRoot chooses a writable cache directory for GitHub, Gitea, and + * local debugging. The priority favors runner-managed tool cache storage when + * the hosting platform exposes it. + */ +export function defaultCacheRoot(): string { + const runnerToolCache: string | undefined = process.env.RUNNER_TOOL_CACHE; + const localAppData: string | undefined = process.env.LOCALAPPDATA; + + if (undefined !== runnerToolCache && "" !== runnerToolCache) { + return path.join(runnerToolCache, "wdk7"); + } + + if (undefined !== localAppData && "" !== localAppData) { + return path.join(localAppData, "actions-tool-cache", "wdk7"); + } + + return path.join(os.tmpdir(), "actions-tool-cache", "wdk7"); +} + +/** + * replaceEnvironmentToken resolves one %NAME% placeholder. Missing environment + * variables expand to an empty string because that mirrors cmd.exe-style + * behavior and prevents unresolved tokens from becoming accidental directories. + */ +function replaceEnvironmentToken(_match: string, name: string): string { + const replacement: string | undefined = process.env[name]; + + if (undefined === replacement) { + return ""; + } + + return replacement; +} diff --git a/src/process.ts b/src/process.ts new file mode 100644 index 0000000..9f5a12e --- /dev/null +++ b/src/process.ts @@ -0,0 +1,83 @@ +import * as core from "@actions/core"; +import { spawn } from "node:child_process"; + +import type { RunOptions } from "./types.js"; + +/** + * runProcess executes a child process and returns trimmed stdout. The action + * uses native Windows tools for ISO mounting and MSI extraction, so this helper + * centralizes logging, hidden-window behavior, and error reporting. + */ +export function runProcess(command: string, args: string[], options?: RunOptions): Promise { + /** + * runProcessPromise bridges event-based child process APIs into async/await. + * The closure keeps stdout and stderr buffers local to exactly one process. + */ + function runProcessPromise(resolve: (value: string) => void, reject: (reason?: unknown) => void): void { + const cwd: string | undefined = undefined !== options ? options.cwd : undefined; + const silent: boolean = undefined !== options && true === options.silent; + + core.debug(`Running: ${command} ${args.join(" ")}`); + + // Windows runner services should not open visible helper windows during ISO + // mounting or MSI extraction because there is no interactive desktop to use. + const child = spawn(command, args, { + cwd: cwd, + windowsHide: true + }); + + let stdout: string = ""; + let stderr: string = ""; + + /** + * onStdoutData mirrors stdout unless the caller requested a quiet command. + * Capturing always continues so callers can parse drive letters or command + * diagnostics even when log output is suppressed. + */ + child.stdout.on("data", function onStdoutData(chunk: Buffer): void { + const text: string = chunk.toString(); + stdout = stdout + text; + + if (false === silent) { + process.stdout.write(text); + } + }); + + /** + * onStderrData preserves stderr for failure messages while respecting the + * same quiet mode used for stdout. + */ + child.stderr.on("data", function onStderrData(chunk: Buffer): void { + const text: string = chunk.toString(); + stderr = stderr + text; + + if (false === silent) { + process.stderr.write(text); + } + }); + + /** + * onChildError reports spawn-level failures, such as a missing executable, + * before a process exit code can exist. + */ + child.on("error", function onChildError(error: Error): void { + reject(error); + }); + + /** + * onChildClose converts process exit status into the promise contract. The + * stderr tail is included because msiexec and PowerShell usually explain + * actionable failures there. + */ + child.on("close", function onChildClose(code: number | null): void { + if (0 === code) { + resolve(stdout.trim()); + return; + } + + reject(new Error(`${command} failed with exit code ${code}. ${stderr.trim()}`)); + }); + } + + return new Promise(runProcessPromise); +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..c7966ef --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,102 @@ +import * as core from "@actions/core"; + +import { uniqueStrings } from "./lists.js"; +import type { ActionInputs } from "./types.js"; + +export const defaultDownloadUrls: string[] = [ + "https://download.microsoft.com/download/4/A/2/4A25C7D5-EFBE-4182-B6A9-AE6850409A78/GRMWDK_EN_7600_1.ISO" +]; + +export const cmakeGenerator: string = "NMake Makefiles"; +export const wdkOnlyCacheKey: string = "wdk-7600.16385.1"; +export const debuggerCacheKey: string = "wdk-7600.16385.1-debugger"; +export const downloadRetries: number = 3; + +/** + * readInputs converts raw GitHub action inputs into the small configuration + * object used by the action. The built-in WDK7 URL is appended here so the rest + * of the code can treat download sources as one ordered list. + */ +export function readInputs(): ActionInputs { + const configuredDownloadUrls: string[] = splitDownloadUrls(core.getInput("download-url")); + const downloadUrls: string[] = uniqueStrings(configuredDownloadUrls.concat(defaultDownloadUrls)); + const root: string = core.getInput("root"); + const debuggerEnabled: boolean = readBooleanInput("debugger", false); + + return { + root: root, + downloadUrls: downloadUrls, + debugger: debuggerEnabled + }; +} + +/** + * cacheKeyForDebugger selects the cache namespace that matches the requested + * SDK surface. Debugger-enabled runs need a distinct key because the cache may + * contain extra DbgEng headers and libraries. + */ +export function cacheKeyForDebugger(debuggerEnabled: boolean): string { + // A debugger cache can safely contain the base WDK, but the reverse is not true. + if (true === debuggerEnabled) { + return debuggerCacheKey; + } + + return wdkOnlyCacheKey; +} + +/** + * restoreKeysForDebugger allows a debugger run to reuse a WDK-only cache first. + * This avoids downloading the large ISO twice when the only missing pieces are + * Debugging Tools files. + */ +export function restoreKeysForDebugger(debuggerEnabled: boolean): string[] { + if (true === debuggerEnabled) { + return [wdkOnlyCacheKey]; + } + + return []; +} + +/** + * splitDownloadUrls accepts the separators people commonly use in workflow + * YAML. The parser is intentionally small because these values are only URLs, + * not a general configuration language. + */ +function splitDownloadUrls(value: string): string[] { + const result: string[] = []; + const parts: string[] = value.split(/[\r\n,;]+/); + + for (const part of parts) { + const trimmed: string = part.trim(); + + if ("" !== trimmed) { + result.push(trimmed); + } + } + + return result; +} + +/** + * readBooleanInput validates boolean action inputs before they can affect the + * install flow. GitHub inputs arrive as strings, so rejecting unexpected values + * makes workflow mistakes fail with a direct message. + */ +function readBooleanInput(name: string, defaultValue: boolean): boolean { + const rawValue: string = core.getInput(name); + const value: string = rawValue.trim().toLowerCase(); + + if ("" === value) { + return defaultValue; + } + + if ("true" === value) { + return true; + } + + if ("false" === value) { + return false; + } + + throw new Error(`Input '${name}' must be true or false.`); +} diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..70a99a7 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,53 @@ +/** + * ActionInputs is the validated action configuration after GitHub input parsing. + * Keeping this shape in one place makes the orchestration code read like a + * business flow instead of a collection of unrelated string lookups. + */ +export interface ActionInputs { + root: string; + downloadUrls: string[]; + debugger: boolean; +} + +/** + * WdkRoot identifies a usable WDK7 tree and records how the action found it. + * The source value is intentionally human-readable because it is also exposed + * as an action output for CI diagnostics. + */ +export interface WdkRoot { + root: string; + source: string; +} + +/** + * DebuggersSdk contains the DbgEng SDK paths that later build steps need. + * The SDK is separate from the generic WDK surface because Debugging Tools are + * optional and should not change normal compiler include/library search order. + */ +export interface DebuggersSdk { + root: string; + includeDir: string; + libI386: string; + libAmd64: string; + binX86: string; + binX64: string; +} + +/** + * PreparedDebuggers reports both the discovered SDK and whether extraction + * changed the cache directory. The cache flag lets the caller save only when + * the action created new material worth preserving. + */ +export interface PreparedDebuggers { + sdk?: DebuggersSdk; + cacheChanged: boolean; +} + +/** + * RunOptions keeps process execution policy explicit at the call site. + * Silent commands still capture output, but do not mirror it to the job log. + */ +export interface RunOptions { + cwd?: string; + silent?: boolean; +} diff --git a/src/wdk.ts b/src/wdk.ts new file mode 100644 index 0000000..480833f --- /dev/null +++ b/src/wdk.ts @@ -0,0 +1,212 @@ +import { existsSync, readdirSync, statSync } from "node:fs"; +import * as path from "node:path"; + +import { fullPath, hostBin, targetBins } from "./paths.js"; +import type { WdkRoot } from "./types.js"; + +/** + * isWdk7Root validates the files required to compile both x86 and amd64 WDK7 + * targets. The action intentionally checks tools and include roots instead of + * trusting directory names because caches may contain nested extraction layouts. + */ +export function isWdk7Root(root: string): boolean { + if ("" === root.trim()) { + return false; + } + + const resolved: string = fullPath(root); + const required: string[] = requiredWdk7Files(resolved); + + for (const requiredPath of required) { + if (false === existsSync(requiredPath)) { + return false; + } + } + + return true; +} + +/** + * findWdk7Root checks the known high-confidence locations in priority order. + * Inputs and environment variables win over caches so self-hosted runners can + * intentionally pin a local WDK installation. + */ +export function findWdk7Root( + requestedRoot: string, + cacheRoot: string, + includeCache: boolean +): WdkRoot | undefined { + const candidates: WdkRoot[] = []; + + addCandidate(candidates, requestedRoot, "input"); + addCandidate(candidates, process.env.WDK7_ROOT, "environment"); + addCandidate(candidates, process.env.W7BASE, "environment"); + + if (true === includeCache) { + addCacheCandidates(candidates, cacheRoot); + } + + addCandidate(candidates, "C:\\WinDDK\\7600.16385.1", "default"); + addCandidate(candidates, "C:\\WinDDK\\7600.16385.win7_wdk.100208-1538", "default"); + + for (const candidate of candidates) { + if (true === isWdk7Root(candidate.root)) { + return candidate; + } + } + + return undefined; +} + +/** + * findWdk7RootUnder scans a directory tree for a valid extracted WDK root. MSI + * administrative extraction can produce nested layouts, so finding setenv.bat + * and then validating the surrounding tree is more reliable than assuming one + * exact directory name. + */ +export function findWdk7RootUnder(basePath: string): string | undefined { + const resolvedBase: string = fullPath(basePath); + + if ("" === resolvedBase || false === existsSync(resolvedBase)) { + return undefined; + } + + if (true === isWdk7Root(resolvedBase)) { + return resolvedBase; + } + + const stack: string[] = [resolvedBase]; + + while (0 < stack.length) { + const current: string | undefined = stack.pop(); + + if (undefined === current) { + continue; + } + + const entries: string[] = readDirectoryEntries(current); + + for (const entry of entries) { + const entryPath: string = path.join(current, entry); + const stats = readStats(entryPath); + + if (undefined === stats) { + continue; + } + + if (true === stats.isDirectory()) { + stack.push(entryPath); + } else if ("setenv.bat" === entry.toLowerCase()) { + const root: string = path.dirname(path.dirname(entryPath)); + + if (true === isWdk7Root(root)) { + return fullPath(root); + } + } + } + } + + return undefined; +} + +/** + * findCachedWdk7Root searches the cache root when the known cache paths did not + * match. This handles older cache layouts without making the primary candidate + * list harder to read. + */ +export function findCachedWdk7Root(cacheRoot: string): WdkRoot | undefined { + const root: string | undefined = findWdk7RootUnder(cacheRoot); + + if (undefined === root) { + return undefined; + } + + return { + root: root, + source: "cache" + }; +} + +/** + * requiredWdk7Files lists the minimum toolchain surface needed by this action. + * The generated list stays explicit so a future WDK compatibility change can + * see exactly which files define "usable" here. + */ +function requiredWdk7Files(root: string): string[] { + const required: string[] = [ + path.join(root, "bin", "setenv.bat"), + path.join(root, "inc", "api"), + path.join(root, "inc", "ddk"), + path.join(hostBin(root), "nmake.exe"), + path.join(hostBin(root), "rc.exe") + ]; + + for (const bin of targetBins(root)) { + required.push(path.join(bin, "cl.exe")); + required.push(path.join(bin, "link.exe")); + } + + return required; +} + +/** + * addCacheCandidates records the cache layouts that have appeared in previous + * action versions and extraction flows. Keeping them grouped avoids burying + * cache compatibility details in the main search order. + */ +function addCacheCandidates(candidates: WdkRoot[], cacheRoot: string): void { + addCandidate(candidates, path.join(cacheRoot, "7600.16385.1"), "cache"); + addCandidate(candidates, path.join(cacheRoot, "7600.16385.win7_wdk.100208-1538"), "cache"); + addCandidate(candidates, path.join(cacheRoot, "wdk7", "7600.16385.1"), "cache"); + addCandidate(candidates, path.join(cacheRoot, "wdk7", "7600.16385.win7_wdk.100208-1538"), "cache"); +} + +/** + * addCandidate normalizes and deduplicates possible WDK roots. Windows paths + * are compared case-insensitively because runner filesystem casing should not + * change detection behavior. + */ +function addCandidate(candidates: WdkRoot[], root: string | undefined, source: string): void { + if (undefined === root || "" === root.trim()) { + return; + } + + const resolved: string = fullPath(root); + const key: string = resolved.toLowerCase(); + + for (const candidate of candidates) { + if (key === candidate.root.toLowerCase()) { + return; + } + } + + candidates.push({ + root: resolved, + source: source + }); +} + +/** + * readDirectoryEntries prevents unreadable directories from aborting WDK cache + * discovery. A stale or partially extracted cache should be skipped, not treated + * as a fatal action error. + */ +function readDirectoryEntries(directory: string): string[] { + try { + return readdirSync(directory); + } catch { + return []; + } +} + +/** + * readStats wraps statSync for the same reason as readDirectoryEntries: cache + * probing must tolerate broken intermediate paths and keep searching. + */ +function readStats(filePath: string): ReturnType | undefined { + try { + return statSync(filePath); + } catch { + return undefined; + } +} diff --git a/test/e2e/cmake/dbgeng.c b/test/e2e/cmake/dbgeng.c index 5134e35..4243ef1 100644 --- a/test/e2e/cmake/dbgeng.c +++ b/test/e2e/cmake/dbgeng.c @@ -1,7 +1,21 @@ #include +/* + * main references an SDK type from dbgeng.h. The fixture validates include and + * library discovery without creating a debugger client at runtime. + */ int main(void) { IDebugClient *client = 0; - return client != 0; + + /* + * The pointer intentionally stays null. The compile/link step is the signal + * under test, and creating a real debugger client would add CI-only runtime + * requirements. + */ + if (0 != client) { + return 1; + } + + return 0; } diff --git a/test/e2e/cmake/dll.c b/test/e2e/cmake/dll.c index 86d8c3a..2d1cb73 100644 --- a/test/e2e/cmake/dll.c +++ b/test/e2e/cmake/dll.c @@ -1,14 +1,32 @@ #include +/* + * DllMain is the minimal DLL entry point required by the linker. The fixture + * intentionally ignores loader events because CI only validates that WDK7 can + * produce a DLL. + */ BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID reserved) { + /* + * These parameters are required by the Windows loader contract but unused in + * this compile/link fixture. + */ UNREFERENCED_PARAMETER(instance); UNREFERENCED_PARAMETER(reason); UNREFERENCED_PARAMETER(reserved); + return TRUE; } +/* + * e2e_answer gives the DLL a simple exported symbol. An export verifies that the + * produced binary is more than an empty loader stub. + */ __declspec(dllexport) int e2e_answer(void) { + /* + * A stable literal is enough for link/export validation and avoids pulling + * any runtime dependency into the fixture. + */ return 7; } diff --git a/test/e2e/cmake/exe.c b/test/e2e/cmake/exe.c index 7eabd90..da11611 100644 --- a/test/e2e/cmake/exe.c +++ b/test/e2e/cmake/exe.c @@ -1,6 +1,14 @@ #include +/* + * main is a minimal user-mode fixture entry point. The test only needs to prove + * that WDK7 can compile and link an executable through the CMake toolchain. + */ int main(void) { + /* + * Returning success keeps the fixture focused on toolchain behavior rather + * than runtime behavior. + */ return 0; } diff --git a/test/e2e/cmake/fetch_dep/dep.c b/test/e2e/cmake/fetch_dep/dep.c index c417392..173dacd 100644 --- a/test/e2e/cmake/fetch_dep/dep.c +++ b/test/e2e/cmake/fetch_dep/dep.c @@ -2,7 +2,20 @@ #include "dep.h" +/* + * e2e_dep_value returns a sentinel from the fetched dependency fixture. The + * DWORD size check keeps the object tied to Windows headers so the dependency + * build exercises the selected WDK include path. + */ int e2e_dep_value(void) { - return sizeof(DWORD) == 4 ? 42 : 0; + /* + * DWORD should remain 32-bit under the WDK7 headers. If that assumption is + * broken, returning zero makes the caller fail the e2e run. + */ + if (4 == sizeof(DWORD)) { + return 42; + } + + return 0; } diff --git a/test/e2e/cmake/fetch_dep/dep.h b/test/e2e/cmake/fetch_dep/dep.h index dd30061..1455393 100644 --- a/test/e2e/cmake/fetch_dep/dep.h +++ b/test/e2e/cmake/fetch_dep/dep.h @@ -1,6 +1,10 @@ #ifndef __E2E_FETCH_DEP_HEADER_FILE__ #define __E2E_FETCH_DEP_HEADER_FILE__ +/* + * e2e_dep_value exposes the fetched dependency sentinel to the native fixture. + * Keeping the declaration in a header makes the link dependency explicit. + */ int e2e_dep_value(void); #endif diff --git a/test/e2e/cmake/lib.c b/test/e2e/cmake/lib.c index 2945040..1341094 100644 --- a/test/e2e/cmake/lib.c +++ b/test/e2e/cmake/lib.c @@ -1,4 +1,12 @@ +/* + * e2e_add gives the static-library fixture a callable symbol. The body is + * intentionally simple because the test is about archive creation, not math. + */ int e2e_add(int left, int right) { + /* + * Returning the sum keeps the object file useful without introducing any + * platform-specific runtime dependency. + */ return left + right; } diff --git a/test/e2e/cmake/native.c b/test/e2e/cmake/native.c index 3752059..688e905 100644 --- a/test/e2e/cmake/native.c +++ b/test/e2e/cmake/native.c @@ -1,6 +1,18 @@ #include "dep.h" +/* + * main verifies that a target can link against the FetchContent dependency + * built by the same WDK7 CMake toolchain. + */ int main(void) { - return e2e_dep_value() == 42 ? 0 : 1; + /* + * The dependency returns a sentinel value, which proves the fixture linked + * the generated static library instead of only compiling this source file. + */ + if (42 == e2e_dep_value()) { + return 0; + } + + return 1; } diff --git a/test/e2e/cmake/sys.c b/test/e2e/cmake/sys.c index dc7cf3d..8f8b8e3 100644 --- a/test/e2e/cmake/sys.c +++ b/test/e2e/cmake/sys.c @@ -1,8 +1,18 @@ #include +/* + * DriverEntry is the required entry point for the CMake kernel-mode fixture. + * The driver performs no runtime work because CI only needs to validate the WDK7 + * compiler, linker flags, and .sys output. + */ NTSTATUS DriverEntry(PDRIVER_OBJECT driver_object, PUNICODE_STRING registry_path) { + /* + * The WDK entry signature requires these parameters even though this fixture + * does not create devices or read registry configuration. + */ UNREFERENCED_PARAMETER(driver_object); UNREFERENCED_PARAMETER(registry_path); + return STATUS_SUCCESS; } diff --git a/test/e2e/ddkbuild/sys/driver.c b/test/e2e/ddkbuild/sys/driver.c index dc7cf3d..8b07a28 100644 --- a/test/e2e/ddkbuild/sys/driver.c +++ b/test/e2e/ddkbuild/sys/driver.c @@ -1,8 +1,18 @@ #include +/* + * DriverEntry is the required entry point for the ddkbuild compatibility + * fixture. The driver stays inert so the test isolates wrapper/toolchain + * behavior from driver runtime behavior. + */ NTSTATUS DriverEntry(PDRIVER_OBJECT driver_object, PUNICODE_STRING registry_path) { + /* + * The parameters are part of the kernel entry contract, but this fixture has + * no device or registry setup to perform. + */ UNREFERENCED_PARAMETER(driver_object); UNREFERENCED_PARAMETER(registry_path); + return STATUS_SUCCESS; }