diff --git a/package-lock.json b/package-lock.json index a660ec7..bd2edcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gulp-msbuild", - "version": "1.0.0", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gulp-msbuild", - "version": "1.0.0", + "version": "1.0.2", "license": "MIT", "dependencies": { "colorette": "^2.0.20", diff --git a/src/msbuild/msbuild-finder.ts b/src/msbuild/msbuild-finder.ts index 008268e..10823b8 100644 --- a/src/msbuild/msbuild-finder.ts +++ b/src/msbuild/msbuild-finder.ts @@ -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)) { @@ -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 [ diff --git a/test/msbuild-finder.test.ts b/test/msbuild-finder.test.ts index 6955ac4..46161b3 100644 --- a/test/msbuild-finder.test.ts +++ b/test/msbuild-finder.test.ts @@ -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' } ]; @@ -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', () => { @@ -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); @@ -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'); + }); + }); +});