Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 41 additions & 2 deletions src/msbuild/msbuild-finder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ export class MSBuildFinder {
if (isNaN(parsedNumber))
throw new Error('version unknown');

// Pre-v12: MSBuild ships with the .NET Framework and does not require vswhere
if (parsedNumber < 12) {
const frameworkPath = this.concatPreV12(this.version);
if (fs.existsSync(frameworkPath)) {
return [frameworkPath, this.version];
}
return null;
}

const vswherePath = path.join(process.env['ProgramFiles(x86)'] || '', 'Microsoft Visual Studio', 'Installer', 'vswhere.exe');

if (!fs.existsSync(vswherePath)) {
Expand All @@ -36,8 +45,38 @@ export class MSBuildFinder {
try {
const installationsBuffer = execSync(`"${vswherePath}" -products * -requires Microsoft.Component.MSBuild -format json`);
const installations = JSON.parse(installationsBuffer.toString());

const installationPaths = installations.map((installation: any) => this.concatCorrectPath(installation.installationPath)) as string[];

const major = (Number)(this.version.split('.')[0]);

if (major >= 16) {
// For v16+, match by the major part of installationVersion rather than
// relying on the installation path containing a particular substring.
// This correctly handles VS 18 (and any future VS version) regardless
// of the year/name used in the installation directory.
const matchingInstallation = installations.find((installation: any) => {
const installMajor = parseInt(
(installation.installationVersion || '').split('.')[0] || '0'
);
return installMajor === major;
});

if (!matchingInstallation) {
return [null, this.version];
}

const msbuildPath = path.join(
matchingInstallation.installationPath,
'MSBuild', 'Current', 'Bin',
this.getx64_dir(),
'MSBuild.exe'
);
return [fs.existsSync(msbuildPath) ? msbuildPath : null, this.version];
}

// v12–v15: use path-based detection via vswhere installation paths
const installationPaths = installations.map(
(installation: any) => this.concatCorrectPath(installation.installationPath)
) as string[];
const installedPath = this.getInstalledVersion(installationPaths);

return [
Expand Down
185 changes: 181 additions & 4 deletions test/msbuild-finder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ describe('MSBuildFinder', () => {
it('should find version using vswhere.exe', () => {
const mockInstallations = [
{
installationPath: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community'
installationPath: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community',
installationVersion: '15.0.0.0'
}
];

Expand Down Expand Up @@ -171,6 +172,154 @@ describe('MSBuildFinder', () => {

expect(result).toBeNull();
});

it('should find pre-v12 version without requiring vswhere.exe', () => {
mockOptions = new MSBuildOptions({
toolsVersion: '4.0',
windir: 'C:\\Windows' as any
});

// existsSync returns true only for the .NET Framework path (no vswhere)
mockFs.existsSync.mockImplementation((p: any) => {
return String(p).includes('Framework');
});

const finder = new MSBuildFinder(mockOptions);
const result = finder.findVersion();

// vswhere should NOT have been called for a pre-v12 version
expect(mockExecSync).not.toHaveBeenCalled();
expect(result).not.toBeNull();
expect(result![0]).toContain('MSBuild.exe');
expect(result![1]).toBe('4.0');
});

it('should return null when pre-v12 .NET Framework path does not exist', () => {
mockOptions = new MSBuildOptions({
toolsVersion: '4.0',
windir: 'C:\\Windows' as any
});

mockFs.existsSync.mockReturnValue(false);

const finder = new MSBuildFinder(mockOptions);
const result = finder.findVersion();

expect(mockExecSync).not.toHaveBeenCalled();
expect(result).toBeNull();
});

it('should find MSBuild 18.0 by matching installationVersion major number', () => {
mockOptions = new MSBuildOptions({
toolsVersion: '18.0',
windir: 'C:\\Windows' as any
});

// Simulate a VS 18 installation whose path may not contain '18' as a substring
const mockInstallations = [
{
installationPath: 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community',
installationVersion: '17.8.0.0'
},
{
installationPath: 'C:\\Program Files\\Microsoft Visual Studio\\2026\\Community',
installationVersion: '18.0.0.0'
}
];

// vswhere.exe itself must also "exist" to allow the lookup to proceed
mockFs.existsSync.mockImplementation((p: any) => {
const s = String(p);
return s.includes('vswhere') || s.includes('MSBuild');
});
mockExecSync.mockReturnValue(Buffer.from(JSON.stringify(mockInstallations)));

const finder = new MSBuildFinder(mockOptions);
const result = finder.findVersion();

expect(result).not.toBeNull();
expect(result![0]).toContain('2026');
expect(result![0]).toContain('MSBuild.exe');
expect(result![1]).toBe('18.0');
});

it('should return null path when no VS 18 installation is found', () => {
mockOptions = new MSBuildOptions({
toolsVersion: '18.0',
windir: 'C:\\Windows' as any
});

// Only VS 17 is installed
const mockInstallations = [
{
installationPath: 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community',
installationVersion: '17.8.0.0'
}
];

mockFs.existsSync.mockReturnValue(true);
mockExecSync.mockReturnValue(Buffer.from(JSON.stringify(mockInstallations)));

const finder = new MSBuildFinder(mockOptions);
const result = finder.findVersion();

expect(result).not.toBeNull();
expect(result![0]).toBeNull();
});

it('should find MSBuild 16.0 (VS 2019) by installationVersion', () => {
mockOptions = new MSBuildOptions({
toolsVersion: '16.0',
windir: 'C:\\Windows' as any
});

const mockInstallations = [
{
installationPath: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community',
installationVersion: '16.11.5.0'
}
];

mockFs.existsSync.mockImplementation((p: any) => {
const s = String(p);
return s.includes('vswhere') || s.includes('MSBuild');
});
mockExecSync.mockReturnValue(Buffer.from(JSON.stringify(mockInstallations)));

const finder = new MSBuildFinder(mockOptions);
const result = finder.findVersion();

expect(result).not.toBeNull();
expect(result![0]).toContain('2019');
expect(result![1]).toBe('16.0');
});

it('should find MSBuild 17.0 (VS 2022) by installationVersion', () => {
mockOptions = new MSBuildOptions({
toolsVersion: '17.0',
windir: 'C:\\Windows' as any
});

const mockInstallations = [
{
installationPath: 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community',
installationVersion: '17.8.0.0'
}
];

mockFs.existsSync.mockImplementation((p: any) => {
const s = String(p);
return s.includes('vswhere') || s.includes('MSBuild');
});
mockExecSync.mockReturnValue(Buffer.from(JSON.stringify(mockInstallations)));

const finder = new MSBuildFinder(mockOptions);
const result = finder.findVersion();

expect(result).not.toBeNull();
expect(result![0]).toContain('2022');
expect(result![1]).toBe('17.0');
});
});

describe('concatCorrectPath', () => {
Expand Down Expand Up @@ -418,7 +567,10 @@ describe('MSBuildFinder', () => {

it('should return latest version when multiple versions are found', () => {
const mockInstallations = [
{ installationPath: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community' }
{
installationPath: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community',
installationVersion: '16.11.5.0'
}
];

mockFs.existsSync.mockReturnValue(true);
Expand All @@ -438,7 +590,32 @@ describe('MSBuildFinder', () => {
expect(result[1]).toBeDefined();
}
});
});
});

it('should auto-detect MSBuild 18 when a VS 18 installation is present', () => {
const mockInstallations = [
{
installationPath: 'C:\\Program Files\\Microsoft Visual Studio\\2026\\Community',
installationVersion: '18.0.0.0'
}
];

mockFs.existsSync.mockImplementation((p: any) => {
const s = String(p);
return s.includes('vswhere') || s.includes('MSBuild');
});
mockExecSync.mockReturnValue(Buffer.from(JSON.stringify(mockInstallations)));

mockOptions = new MSBuildOptions({
toolsVersion: 'auto' as any,
windir: 'C:\\Windows' as any
});

const finder = new MSBuildFinder(mockOptions);
const result = finder.getLatestAvailableVersion();

expect(result).not.toBeNull();
expect(result![1]).toBe('18.0');
expect(result![0]).toContain('MSBuild.exe');
});
});
});
Loading