From fc127f2e6889b65437682bd837c28d958accc004 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Mon, 21 Sep 2026 16:28:32 -0500 Subject: [PATCH] Default to auto-detected 64-bit MSBuild on 64-bit hosts (breaking change, v4) Fixes #88 - msbuild-architecture no longer hardcodes a default of "x86". When the input is left unspecified, the action now auto-detects: it prefers x64 when running on a 64-bit machine and the resolved VS/MSBuild install is 17.0+ (VS 2022+), and falls back to x86 otherwise. An explicit msbuild-architecture value is always respected. - Extracted resolveMSBuildArchitecture()/parseMajorVersion() as pure, exported helpers and added jest tests covering the resolution matrix. - Bumped package.json version to 4.0.0 (breaking change/new major version). - Updated action.yml input description and README (usage examples now reference @v4, new "Breaking Changes in v4" section, and the architecture-selection docs rewritten to describe auto-detection). - Rebuilt dist/index.js via ncc. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 30 ++++++++++++--- __tests__/main.test.ts | 54 +++++++++++++++++++++++++++ action.yml | 3 +- dist/index.js | 55 ++++++++++++++++++++++++++- package.json | 2 +- src/main.ts | 85 ++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 __tests__/main.test.ts diff --git a/README.md b/README.md index 1b8ceab..afecd4d 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,25 @@ This action will help discover where the `MSBuild` tool is and automatically add > [!IMPORTANT] > Please note this tool does NOT add other Visual Studio tools (like VSTest, cl, cmake, or others) to `PATH` +## Breaking Changes in v4 + +Starting with `v4`, the default value of `msbuild-architecture` is no longer always `x86`. When +`msbuild-architecture` is not explicitly specified, the action now **auto-detects** the best +architecture: + +- `x64` when running on a 64-bit machine **and** the resolved Visual Studio/MSBuild install is + version 17.0 or later (Visual Studio 2022+, which ships a native 64-bit MSBuild). +- `x86` otherwise (older Visual Studio versions, or 32-bit hosts), matching the previous behavior. + +If you explicitly set `msbuild-architecture` (e.g. `x86`, `x64`, or `arm64`), that value is always +respected and this auto-detection does not apply. If your workflow depends on always getting the +32-bit MSBuild by default, set `msbuild-architecture: x86` explicitly. + ## Example Usage ```yml - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v3 + uses: microsoft/setup-msbuild@v4 - name: Build app for release run: msbuild src\YourProjectFile.csproj -t:rebuild -verbosity:diag -property:Configuration=Release @@ -29,7 +43,7 @@ You may have a situation where your Actions runner has multiple versions of Visu ```yml - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v3 + uses: microsoft/setup-msbuild@v4 with: vs-version: '[16.4,16.5)' ``` @@ -42,18 +56,22 @@ If you need your Actions runner to target a pre-release version of Visual Studio ```yml - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v3 + uses: microsoft/setup-msbuild@v4 with: vs-prerelease: true ``` ### Specifying MSBuild architecture (optional) -By default the action will use the x86 architecture for MSBuild, but it is possible to target the x64 versions instead. Simply add the `msbuild-architecture` input. Valid input values are `x86` (default), `x64`, and `arm64`. Note that the success of these will rely on the runner OS. +By default (as of `v4`), the action auto-detects the preferred MSBuild architecture: it uses `x64` +when running on a 64-bit machine with Visual Studio/MSBuild 17.0 or later installed, and falls back +to `x86` otherwise. You can override this by explicitly setting the `msbuild-architecture` input. +Valid input values are `x86`, `x64`, and `arm64`. Note that the success of these will rely on the +runner OS. ```yml - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v3 + uses: microsoft/setup-msbuild@v4 with: msbuild-architecture: x64 ``` @@ -64,7 +82,7 @@ This makes use of the vswhere tool which is a tool delivered by Microsoft to hel ```yml - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v3 + uses: microsoft/setup-msbuild@v4 with: vswhere-path: 'C:\path\to\your\tools\' ``` diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts new file mode 100644 index 0000000..742c75e --- /dev/null +++ b/__tests__/main.test.ts @@ -0,0 +1,54 @@ +import { + MIN_VS_VERSION_FOR_X64, + parseMajorVersion, + resolveMSBuildArchitecture +} from '../src/main' + +describe('parseMajorVersion', () => { + it('parses a standard installationVersion string', () => { + expect(parseMajorVersion('17.9.34728.123')).toBe(17) + }) + + it('parses a single-segment version', () => { + expect(parseMajorVersion('16')).toBe(16) + }) + + it('trims whitespace before parsing', () => { + expect(parseMajorVersion(' 17.0.0.0 ')).toBe(17) + }) + + it('returns undefined for an empty string', () => { + expect(parseMajorVersion('')).toBeUndefined() + }) + + it('returns undefined for a non-numeric string', () => { + expect(parseMajorVersion('not-a-version')).toBeUndefined() + }) +}) + +describe('resolveMSBuildArchitecture', () => { + it('always respects an explicit architecture, regardless of OS/version', () => { + expect(resolveMSBuildArchitecture('x86', true, 17)).toBe('x86') + expect(resolveMSBuildArchitecture('x64', false, 15)).toBe('x64') + expect(resolveMSBuildArchitecture('arm64', true, 17)).toBe('arm64') + }) + + it('defaults to x64 on a 64-bit host with VS/MSBuild 17+', () => { + expect(resolveMSBuildArchitecture('', true, MIN_VS_VERSION_FOR_X64)).toBe( + 'x64' + ) + expect(resolveMSBuildArchitecture('', true, 18)).toBe('x64') + }) + + it('defaults to x86 on a 64-bit host with VS/MSBuild older than 17', () => { + expect(resolveMSBuildArchitecture('', true, 16)).toBe('x86') + }) + + it('defaults to x86 on a non-64-bit host even with VS/MSBuild 17+', () => { + expect(resolveMSBuildArchitecture('', false, 17)).toBe('x86') + }) + + it('defaults to x86 when the VS/MSBuild version could not be determined', () => { + expect(resolveMSBuildArchitecture('', true, undefined)).toBe('x86') + }) +}) diff --git a/action.yml b/action.yml index 9b2d4fb..04f4c14 100644 --- a/action.yml +++ b/action.yml @@ -15,9 +15,8 @@ inputs: description: "Enable searching for pre-release versions of Visual Studio/MSBuild" required: false msbuild-architecture: - description: 'The preferred processor architecture of MSBuild. Can be either "x86", "x64", or "arm64". "x64" is only available from Visual Studio version 17.0 and later.' + description: 'The preferred processor architecture of MSBuild. Can be either "x86", "x64", or "arm64". "x64" is only available from Visual Studio version 17.0 and later. If left unspecified, the action auto-detects the best option: "x64" on 64-bit machines when a Visual Studio/MSBuild 17.0+ install is found, otherwise "x86".' required: false - default: "x86" outputs: msbuildPath: description: "The resulting location of msbuild for your inputs" diff --git a/dist/index.js b/dist/index.js index af3aeb0..7a2873a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -26,13 +26,45 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); const core = __importStar(__nccwpck_require__(484)); const exec = __importStar(__nccwpck_require__(236)); const fs = __importStar(__nccwpck_require__(896)); +const os = __importStar(__nccwpck_require__(857)); const path = __importStar(__nccwpck_require__(928)); const io = __importStar(__nccwpck_require__(994)); const IS_WINDOWS = process.platform === 'win32'; const VS_VERSION = core.getInput('vs-version') || 'latest'; const VSWHERE_PATH = core.getInput('vswhere-path'); const ALLOW_PRERELEASE = core.getInput('vs-prerelease') || 'false'; -let MSBUILD_ARCH = core.getInput('msbuild-architecture') || 'x86'; +const MSBUILD_ARCH_INPUT = core.getInput('msbuild-architecture'); +let MSBUILD_ARCH = MSBUILD_ARCH_INPUT; +// The minimum Visual Studio/MSBuild major version that ships a native x64 MSBuild.exe +exports.MIN_VS_VERSION_FOR_X64 = 17; +// Determines the effective MSBuild architecture to use. +// If the user explicitly specified `msbuild-architecture`, that choice is always respected. +// Otherwise (breaking change as of v4), the action auto-detects and prefers x64 when +// running on a 64-bit OS and the resolved VS/MSBuild installation is version 17.0 or later. +// If the VS version can't be determined, or the host isn't 64-bit, it falls back to x86. +function resolveMSBuildArchitecture(explicitArch, isWindows64Bit, vsMajorVersion) { + if (explicitArch) { + return explicitArch; + } + if (isWindows64Bit && + vsMajorVersion !== undefined && + vsMajorVersion >= exports.MIN_VS_VERSION_FOR_X64) { + return 'x64'; + } + return 'x86'; +} +exports.resolveMSBuildArchitecture = resolveMSBuildArchitecture; +// Parses the major version number out of a vswhere `installationVersion` string +// such as "17.9.34728.123". Returns undefined if it cannot be parsed. +function parseMajorVersion(installationVersion) { + const match = installationVersion.trim().match(/^(\d+)/); + if (!match) { + return undefined; + } + const major = parseInt(match[1], 10); + return isNaN(major) ? undefined : major; +} +exports.parseMajorVersion = parseMajorVersion; // if a specific version of VS is requested let VSWHERE_EXEC = '-products * -requires Microsoft.Component.MSBuild -property installationPath -latest '; if (ALLOW_PRERELEASE === 'true') { @@ -75,6 +107,27 @@ function run() { return; } core.debug(`Full tool exe: ${vswhereToolExe}`); + // if the user did not explicitly specify an architecture, auto-detect the + // preferred architecture based on the host OS and the resolved VS/MSBuild version + if (!MSBUILD_ARCH_INPUT) { + let vsMajorVersion; + const versionOptions = {}; + versionOptions.listeners = { + stdout: (data) => { + const installationVersion = data.toString().trim(); + core.debug(`Found installation version: ${installationVersion}`); + const parsed = parseMajorVersion(installationVersion); + if (parsed !== undefined) { + vsMajorVersion = parsed; + } + } + }; + const versionExec = VSWHERE_EXEC.replace('-property installationPath', '-property installationVersion'); + yield exec.exec(`"${vswhereToolExe}" ${versionExec}`, [], versionOptions); + const isWindows64Bit = os.arch() === 'x64' || os.arch() === 'arm64'; + MSBUILD_ARCH = resolveMSBuildArchitecture(MSBUILD_ARCH_INPUT, isWindows64Bit, vsMajorVersion); + core.info(`Auto-detected msbuild-architecture: ${MSBUILD_ARCH} (os.arch=${os.arch()}, vsMajorVersion=${vsMajorVersion})`); + } let foundToolPath = ''; const options = {}; options.listeners = { diff --git a/package.json b/package.json index f6e4b8a..f05e47e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "setup-msbuild", - "version": "3.0.0", + "version": "4.0.0", "private": true, "description": "Helps set up specific MSBuild tool into PATH for later usage.", "main": "lib/main.js", diff --git a/src/main.ts b/src/main.ts index f0a7d8d..4241034 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ import * as core from '@actions/core' import * as exec from '@actions/exec' import * as fs from 'fs' +import * as os from 'os' import * as path from 'path' import * as io from '@actions/io' import {ExecOptions} from '@actions/exec/lib/interfaces' @@ -9,13 +10,56 @@ const IS_WINDOWS = process.platform === 'win32' const VS_VERSION = core.getInput('vs-version') || 'latest' const VSWHERE_PATH = core.getInput('vswhere-path') const ALLOW_PRERELEASE = core.getInput('vs-prerelease') || 'false' -let MSBUILD_ARCH = core.getInput('msbuild-architecture') || 'x86' +const MSBUILD_ARCH_INPUT = core.getInput('msbuild-architecture') +let MSBUILD_ARCH = MSBUILD_ARCH_INPUT + +// The minimum Visual Studio/MSBuild major version that ships a native x64 MSBuild.exe +export const MIN_VS_VERSION_FOR_X64 = 17 + +// Determines the effective MSBuild architecture to use. +// If the user explicitly specified `msbuild-architecture`, that choice is always respected. +// Otherwise (breaking change as of v4), the action auto-detects and prefers x64 when +// running on a 64-bit OS and the resolved VS/MSBuild installation is version 17.0 or later. +// If the VS version can't be determined, or the host isn't 64-bit, it falls back to x86. +export function resolveMSBuildArchitecture( + explicitArch: string, + isWindows64Bit: boolean, + vsMajorVersion: number | undefined +): string { + if (explicitArch) { + return explicitArch + } + + if ( + isWindows64Bit && + vsMajorVersion !== undefined && + vsMajorVersion >= MIN_VS_VERSION_FOR_X64 + ) { + return 'x64' + } + + return 'x86' +} + +// Parses the major version number out of a vswhere `installationVersion` string +// such as "17.9.34728.123". Returns undefined if it cannot be parsed. +export function parseMajorVersion( + installationVersion: string +): number | undefined { + const match = installationVersion.trim().match(/^(\d+)/) + if (!match) { + return undefined + } + const major = parseInt(match[1], 10) + return isNaN(major) ? undefined : major +} // if a specific version of VS is requested -let VSWHERE_EXEC = '-products * -requires Microsoft.Component.MSBuild -property installationPath -latest ' +let VSWHERE_EXEC = + '-products * -requires Microsoft.Component.MSBuild -property installationPath -latest ' if (ALLOW_PRERELEASE === 'true') { - VSWHERE_EXEC += ' -prerelease ' - } + VSWHERE_EXEC += ' -prerelease ' +} if (VS_VERSION !== 'latest') { VSWHERE_EXEC += `-version "${VS_VERSION}" ` @@ -64,6 +108,39 @@ async function run(): Promise { core.debug(`Full tool exe: ${vswhereToolExe}`) + // if the user did not explicitly specify an architecture, auto-detect the + // preferred architecture based on the host OS and the resolved VS/MSBuild version + if (!MSBUILD_ARCH_INPUT) { + let vsMajorVersion: number | undefined + const versionOptions: ExecOptions = {} + versionOptions.listeners = { + stdout: (data: Buffer) => { + const installationVersion = data.toString().trim() + core.debug(`Found installation version: ${installationVersion}`) + const parsed = parseMajorVersion(installationVersion) + if (parsed !== undefined) { + vsMajorVersion = parsed + } + } + } + + const versionExec = VSWHERE_EXEC.replace( + '-property installationPath', + '-property installationVersion' + ) + await exec.exec(`"${vswhereToolExe}" ${versionExec}`, [], versionOptions) + + const isWindows64Bit = os.arch() === 'x64' || os.arch() === 'arm64' + MSBUILD_ARCH = resolveMSBuildArchitecture( + MSBUILD_ARCH_INPUT, + isWindows64Bit, + vsMajorVersion + ) + core.info( + `Auto-detected msbuild-architecture: ${MSBUILD_ARCH} (os.arch=${os.arch()}, vsMajorVersion=${vsMajorVersion})` + ) + } + let foundToolPath = '' const options: ExecOptions = {} options.listeners = {