diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 31b8026..1adc9a8 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -125,21 +125,22 @@ jobs: fail-fast: false matrix: os: [windows-2025, windows-2022] - cmp: [gcc, vs2022] + cmp: [gcc, vs] configuration: [default, static, debug, static-debug] base: [ "7.0" ] include: - os: windows-2025 - cmp: vs2022 + cmp: vs configuration: static base: "3.15" - name: "B-3.15 Win-25 MSC-22 static" + name: "B-3.15 Win-25 MSC-26 static" - os: windows-2025 - cmp: vs2022 + cmp: vs configuration: static base: "3.14" - name: "B-3.14 Win-25 MSC-22 static" + name: "B-3.14 Win-25 MSC-26 static" + steps: - uses: actions/checkout@v7 - name: Prepare and compile dependencies diff --git a/cue-test.py b/cue-test.py index 8e5d84e..d6ba3aa 100644 --- a/cue-test.py +++ b/cue-test.py @@ -342,6 +342,106 @@ def test_Repos(self): self.assertEqual(repo_access(mod), 0, 'Defaults for {0} do not point to a valid git repository at {1}' .format(mod, cue.setup[mod + '_REPOURL'])) +class TestVSCompilerResolution(unittest.TestCase): + def setUp(self): + cue.clear_lists() + # Save original state of variables we will mock + self.orig_ci = dict(cue.ci) + self.orig_vcvars_found = dict(cue.vcvars_found) + self.orig_get_vs_installations_via_vswhere = cue.get_vs_installations_via_vswhere + + def tearDown(self): + # Restore original state + cue.ci.clear() + cue.ci.update(self.orig_ci) + cue.vcvars_found.clear() + cue.vcvars_found.update(self.orig_vcvars_found) + cue.get_vs_installations_via_vswhere = self.orig_get_vs_installations_via_vswhere + + def test_vswhere_single_installation(self): + cue.ci['os'] = 'windows' + cue.ci['compiler'] = 'vs' + + mock_installs = [{ + 'path': r'C:\Program Files\Microsoft Visual Studio\2022\Community', + 'vcvarsall': r'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat', + 'version': '17.4.33103.184', + 'name': 'Visual Studio Community 2022' + }] + cue.get_vs_installations_via_vswhere = lambda: mock_installs + + cue.resolve_vs_compiler() + + self.assertEqual(cue.ci['compiler'], 'vs2022') + self.assertEqual(cue.vcvars_found['vs2022'], mock_installs[0]['vcvarsall']) + + def test_vswhere_multiple_installations(self): + cue.ci['os'] = 'windows' + cue.ci['compiler'] = 'vs' + + mock_installs = [ + { + 'path': r'C:\Program Files\Microsoft Visual Studio\2022\Community', + 'vcvarsall': r'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat', + 'version': '17.4.33103.184', + 'name': 'Visual Studio Community 2022' + }, + { + 'path': r'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community', + 'vcvarsall': r'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat', + 'version': '16.5.30011.22', + 'name': 'Visual Studio Community 2019' + } + ] + cue.get_vs_installations_via_vswhere = lambda: mock_installs + + with self.assertRaisesRegex(ValueError, "Multiple Visual Studio installations found"): + cue.resolve_vs_compiler() + + def test_vswhere_no_installations(self): + cue.ci['os'] = 'windows' + cue.ci['compiler'] = 'vs' + + cue.get_vs_installations_via_vswhere = lambda: [] + + with self.assertRaisesRegex(ValueError, "No Visual Studio installations found"): + cue.resolve_vs_compiler() + + def test_fallback_single_installation(self): + cue.ci['os'] = 'windows' + cue.ci['compiler'] = 'vs' + + cue.get_vs_installations_via_vswhere = lambda: None + cue.vcvars_found = {'vs2019': r'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat'} + + cue.resolve_vs_compiler() + + self.assertEqual(cue.ci['compiler'], 'vs2019') + + def test_fallback_multiple_installations(self): + cue.ci['os'] = 'windows' + cue.ci['compiler'] = 'vs' + + cue.get_vs_installations_via_vswhere = lambda: None + cue.vcvars_found = { + 'vs2019': r'C:\vcvars19.bat', + 'vs2022': r'C:\vcvars22.bat' + } + + with self.assertRaisesRegex(ValueError, "Multiple Visual Studio installations found"): + cue.resolve_vs_compiler() + + def test_fallback_no_installations(self): + cue.ci['os'] = 'windows' + cue.ci['compiler'] = 'vs' + + cue.get_vs_installations_via_vswhere = lambda: None + cue.vcvars_found = {} + + with self.assertRaisesRegex(ValueError, "No Visual Studio installations found"): + cue.resolve_vs_compiler() + + @unittest.skipIf(ci_os != 'windows', 'VCVars test only applies to windows') class TestVCVars(unittest.TestCase): def test_vcvars(self): @@ -349,10 +449,15 @@ def test_vcvars(self): os.environ['TRAVIS_COMPILER'] = 'vs2017' else: os.environ['CONFIGURATION'] = 'default' - if ci_service == 'github-actions' and os.environ['IMAGEOS'] == 'win16': - os.environ['CMP'] = 'vs2017' - elif ci_service == 'github-actions' and os.environ['IMAGEOS'] == 'win19': - os.environ['CMP'] = 'vs2019' + if ci_service == 'github-actions': + if os.environ['IMAGEOS'] == 'win16': + os.environ['CMP'] = 'vs2017' + elif os.environ['IMAGEOS'] == 'win19': + os.environ['CMP'] = 'vs2019' + elif os.environ['IMAGEOS'] == 'win25-vs2026': + os.environ['CMP'] = 'vs2026' + else: + os.environ['CMP'] = 'vs2022' else: os.environ['CMP'] = 'vs2022' cue.detect_context() diff --git a/cue.py b/cue.py index f7bbee9..f529448 100644 --- a/cue.py +++ b/cue.py @@ -70,7 +70,7 @@ def detect_context(): ci['platform'] = 'x64' ci['compiler'] = os.environ['TRAVIS_COMPILER'] ci['choco'] += ['strawberryperl'] - if re.match(r'^vs', ci['compiler']): + if re.match(r'^vs', ci['compiler']) and ci['compiler'] != 'vs': # Only Visual Studio 2017 available ci['compiler'] = 'vs2017' if 'BCFG' in os.environ: @@ -165,12 +165,13 @@ def detect_context(): if 'CLEAN_DEPS' in os.environ and os.environ['CLEAN_DEPS'].lower() == 'no': ci['clean_deps'] = False + resolve_vs_compiler() + logger.debug('Detected a build hosted on %s, using %s on %s (%s) configured as %s ' + '(test: %s, clean_deps: %s)', ci['service'], ci['compiler'], ci['os'], ci['platform'], ci['configuration'], ci['test'], ci['clean_deps']) - curdir = os.getcwd() ci = {} @@ -286,6 +287,8 @@ def __exit__(self,A,B,C): vcvars_table = { # https://en.wikipedia.org/wiki/Microsoft_Visual_Studio#History + 'vs2026': [r'C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvarsall.bat', + r'C:\Program Files\Microsoft Visual Studio\18\Enterprise\VC\Auxiliary\Build\vcvarsall.bat'], 'vs2022': [r'C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat', r'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat'], 'vs2019': [r'C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Auxiliary\Build\vcvarsall.bat', @@ -307,6 +310,148 @@ def __exit__(self,A,B,C): vcvars_found[key] = dir +def find_vswhere(): + if hasattr(shutil, 'which'): + loc = shutil.which('vswhere') + if loc: + return loc + for path in [ + r'C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe', + r'C:\Program Files\Microsoft Visual Studio\Installer\vswhere.exe' + ]: + if os.path.exists(path): + return path + return None + + +def query_vswhere(): + import json + vswhere_path = find_vswhere() + if not vswhere_path: + return None + try: + cmd = [vswhere_path, '-legacy', '-prerelease', '-format', 'json', '-products', '*'] + logger.debug("Running vswhere: %s", ' '.join(cmd)) + output = sp.check_output(cmd).decode('utf-8', errors='ignore') + return json.loads(output) + except Exception as e: + logger.debug("vswhere execution or parsing failed: %s", e) + try: + cmd = [vswhere_path, '-legacy', '-prerelease', '-format', 'json'] + output = sp.check_output(cmd).decode('utf-8', errors='ignore') + return json.loads(output) + except Exception as e2: + logger.debug("vswhere fallback failed: %s", e2) + return None + + +def find_vcvarsall_in_install(install_path): + paths = [ + os.path.join(install_path, r'VC\Auxiliary\Build\vcvarsall.bat'), + os.path.join(install_path, r'VC\vcvarsall.bat') + ] + for p in paths: + if os.path.exists(p): + return p + return None + + +def get_vs_installations_via_vswhere(): + instances = query_vswhere() + if instances is None: + return None + + valid_installs = [] + for inst in instances: + path = inst.get('installationPath') + if not path: + continue + vcvars_path = find_vcvarsall_in_install(path) + if vcvars_path: + version_str = inst.get('installationVersion', '') + name = inst.get('displayName', inst.get('installationName', 'Visual Studio')) + valid_installs.append({ + 'path': path, + 'vcvarsall': vcvars_path, + 'version': version_str, + 'name': name + }) + return valid_installs + + +def resolve_vs_compiler(): + if ci['os'] != 'windows' or ci['compiler'] != 'vs': + return + + installs = get_vs_installations_via_vswhere() + if installs is not None: + if len(installs) == 1: + inst = installs[0] + major_version_str = inst['version'].split('.')[0] + try: + major_version = int(major_version_str) + except ValueError: + major_version = 0 + + VS_VERSION_MAP = { + 15: 'vs2017', + 16: 'vs2019', + 17: 'vs2022', + 18: 'vs2026', + 14: 'vs2015', + 12: 'vs2013', + 11: 'vs2012', + 10: 'vs2010', + 9: 'vs2008' + } + vs_key = VS_VERSION_MAP.get(major_version, 'vs' + major_version_str) + + ci['compiler'] = vs_key + vcvars_found[vs_key] = inst['vcvarsall'] + print("Selected installed Visual Studio compiler: {0} ({1}) at {2}".format(vs_key, inst['name'], inst['vcvarsall'])) + sys.stdout.flush() + elif len(installs) > 1: + print("Multiple Visual Studio installations found:") + for inst in installs: + major_version_str = inst['version'].split('.')[0] + try: + major = int(major_version_str) + except ValueError: + major = 0 + VS_VERSION_MAP = { + 15: 'vs2017', + 16: 'vs2019', + 17: 'vs2022', + 18: 'vs2026', + 14: 'vs2015', + 12: 'vs2013', + 11: 'vs2012', + 10: 'vs2010', + 9: 'vs2008' + } + vs_key = VS_VERSION_MAP.get(major, 'vs' + major_version_str) + print(" {0}: {1} at {2}".format(vs_key, inst['name'], inst['path'])) + sys.stdout.flush() + raise ValueError("Multiple Visual Studio installations found. Please specify one of the available options.") + else: + raise ValueError("No Visual Studio installations found on this system.") + else: + valid_keys = list(vcvars_found.keys()) + if len(valid_keys) == 1: + vs_key = valid_keys[0] + ci['compiler'] = vs_key + print("Selected installed Visual Studio compiler (via fallback list): {0} at {1}".format(vs_key, vcvars_found[vs_key])) + sys.stdout.flush() + elif len(valid_keys) > 1: + print("Multiple Visual Studio installations found in fallback list:") + for k in valid_keys: + print(" {0}: {1}".format(k, vcvars_found[k])) + sys.stdout.flush() + raise ValueError("Multiple Visual Studio installations found. Please specify one of the available options.") + else: + raise ValueError("No Visual Studio installations found in fallback list.") + + def modlist(): if building_base: ret = []