├── .github ├── FUNDING.yml └── workflows │ └── all.yml ├── .gitignore ├── LICENSE ├── README.md ├── cmake_downloader.py ├── cmake_min_version.py ├── pyproject.toml ├── requirements-dev.txt └── requirements.txt /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nlohmann 2 | custom: https://paypal.me/nlohmann 3 | -------------------------------------------------------------------------------- /.github/workflows/all.yml: -------------------------------------------------------------------------------- 1 | name: Cross-platform 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | matrix: 12 | os: [ubuntu-latest, macos-latest, windows-latest] 13 | python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'] 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-python@v4 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Setup virtual environment 20 | run: | 21 | python -m pip install --upgrade pip 22 | python -m pip install wheel 23 | python -m pip install -r requirements.txt 24 | - name: Run cmake_downloader.py 25 | run: python cmake_downloader.py --latest_release 26 | 27 | style: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v3 31 | - uses: actions/setup-python@v4 32 | with: 33 | python-version: 3.12 34 | - name: Setup virtual environment 35 | run: | 36 | python -m pip install --upgrade pip 37 | python -m pip install wheel 38 | python -m pip install -r requirements-dev.txt 39 | - name: Style checks 40 | run: | 41 | black --check *.py 42 | isort --check *.py 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tools/ 2 | /venv/ 3 | /.idea/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2024 Niels Lohmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cmake_min_version 2 | 3 | [![Cross-platform](https://github.com/nlohmann/cmake_min_version/actions/workflows/all.yml/badge.svg)](https://github.com/nlohmann/cmake_min_version/actions/workflows/all.yml) 4 | 5 | Every CMake project requires a call to [`cmake_minimum_required`](http://cmake.org/cmake/help/v3.16/command/cmake_minimum_required.html) to set the minimally required CMake version. However, CMake gives no guidance what this version may be, and a lot of projects just take the current CMake version or whatever the IDE is proposing as default. This is a problem, because some platforms don't always provide the latest CMake version, and a lot of trial and error is needed before projects can be used. 6 | 7 | `cmake_min_version` is a script to determine the minimal working version of CMake for a given project. It does not do any magic, but just performs a binary search using a pool of CMake binaries and basically implements the "trial and error" in an efficient way. 8 | 9 | ## Example 10 | 11 | Assume `~/projects/example` contains a project with a `CMakeLists.txt` file. Then the following call determines the minimal working version of CMake: 12 | 13 | ```sh 14 | ❯ venv/bin/python cmake_min_version.py ~/projects/example 15 | 16 | Found 94 CMake binaries from directory tools 17 | 18 | [ 0%] CMake 3.9.2 ✔ works 19 | [ 12%] CMake 3.2.2 ✘ error 20 | CMakeLists.txt:7 (cmake_minimum_required) 21 | [ 33%] CMake 3.8.0 ✔ works 22 | [ 50%] CMake 3.7.1 ✘ error 23 | CMakeLists.txt:16 (target_compile_features) 24 | [ 80%] CMake 3.7.2 ✘ error 25 | CMakeLists.txt:16 (target_compile_features) 26 | [100%] Minimal working version: CMake 3.8.0 27 | 28 | cmake_minimum_required(VERSION 3.8.0) 29 | ``` 30 | 31 | As a result, `~/projects/example/CMakeLists.txt` could be adjusted to require CMake 3.8.0. 32 | 33 | More options: 34 | 35 | ```sh 36 | usage: cmake_min_version.py [-h] [--tools_directory DIR] [--full_search] [--error_details] 37 | params [params ...] 38 | 39 | Find the minimal required CMake version for a project. 40 | 41 | positional arguments: 42 | params parameters to pass to CMake 43 | 44 | options: 45 | -h, --help show this help message and exit 46 | --tools_directory DIR 47 | path to the CMake binaries (default: "tools") 48 | --full_search Searches using a top down approach instead of a binary search (default: False) 49 | --error_details Print the full stderr output in case of an error (default: False) 50 | ``` 51 | 52 | ## FAQ 53 | 54 | - Q: Isn't this a rather naive and inefficient approach to achieve the goal? 55 | - A: Yes, but I am currently not aware of a better one. I would be happy to replace this repository with a link on a tool that achieves the same goal. 56 | 57 | ## Setup 58 | 59 | ### In a nutshell 60 | 61 | 1. Install a Python virtual requirement (Python 3.6 or later). 62 | 2. Download CMake binaries. 63 | 64 | ### Virtual Environment 65 | 66 | The code requires some [packages](requirements.txt) to be installed: 67 | 68 | ```sh 69 | python3 -mvenv venv 70 | venv/bin/pip3 install -r requirements.txt 71 | ``` 72 | 73 | ### CMake binaries 74 | 75 | The script [`cmake_downloader.py`](cmake_downloader.py) takes care of downloading CMake binaries: 76 | 77 | ```sh 78 | usage: cmake_downloader.py [-h] [--os {macos,linux,windows}] [--latest_release] 79 | [--latest_patch] [--first_minor] 80 | [--release_candidates] [--min_version MIN_VERSION] 81 | [--max_version MAX_VERSION] [--tools_directory DIR] 82 | 83 | Download CMake binaries. 84 | 85 | options: 86 | -h, --help show this help message and exit 87 | --os {macos,linux,windows} 88 | OS to download CMake for (default: linux) 89 | --latest_release only download the latest release (default: False) 90 | --latest_patch only download the latest patch version for each release (default: False) 91 | --first_minor only download the first minor version for each release (default: False) 92 | --release_candidates also consider release candidates (default: False) 93 | --min_version MIN_VERSION 94 | only download versions greater or equal than MIN_VERSION 95 | --max_version MAX_VERSION 96 | only download versions less or equal than MAX_VERSION 97 | --tools_directory DIR 98 | path to the CMake binaries (default: "tools") 99 | ``` 100 | 101 | Example run: 102 | 103 | ```sh 104 | ❯ venv/bin/python3 cmake_downloader.py --latest_patch 105 | Retrieving URLs... 106 | 100%|███████████████████████████████████████████| 32/32 [00:18<00:00, 1.71it/s] 107 | Downloading CMake 2.8.12.2... 108 | 100%|██████████████████████████████████████| 40.5M/40.5M [00:12<00:00, 3.34MB/s] 109 | Downloading CMake 3.0.2... 110 | 100%|██████████████████████████████████████| 38.7M/38.7M [00:10<00:00, 3.90MB/s] 111 | Downloading CMake 3.1.3... 112 | 100%|██████████████████████████████████████| 28.6M/28.6M [00:07<00:00, 3.99MB/s] 113 | Downloading CMake 3.2.3... 114 | 100%|██████████████████████████████████████| 26.4M/26.4M [00:07<00:00, 3.52MB/s] 115 | Downloading CMake 3.3.2... 116 | 100%|██████████████████████████████████████| 21.3M/21.3M [00:06<00:00, 3.68MB/s] 117 | Downloading CMake 3.4.3... 118 | 100%|██████████████████████████████████████| 21.6M/21.6M [00:07<00:00, 3.07MB/s] 119 | Downloading CMake 3.5.2... 120 | 100%|██████████████████████████████████████| 21.8M/21.8M [00:06<00:00, 3.33MB/s] 121 | Downloading CMake 3.6.3... 122 | 100%|██████████████████████████████████████| 24.9M/24.9M [00:08<00:00, 2.92MB/s] 123 | Downloading CMake 3.7.2... 124 | 100%|██████████████████████████████████████| 25.1M/25.1M [00:09<00:00, 2.85MB/s] 125 | Downloading CMake 3.8.2... 126 | 100%|██████████████████████████████████████| 25.2M/25.2M [00:06<00:00, 3.95MB/s] 127 | Downloading CMake 3.9.6... 128 | 100%|██████████████████████████████████████| 25.5M/25.5M [00:07<00:00, 3.41MB/s] 129 | Downloading CMake 3.10.3... 130 | 100%|██████████████████████████████████████| 25.9M/25.9M [00:06<00:00, 3.93MB/s] 131 | Downloading CMake 3.11.4... 132 | 100%|██████████████████████████████████████| 26.1M/26.1M [00:06<00:00, 3.96MB/s] 133 | Downloading CMake 3.12.4... 134 | 100%|██████████████████████████████████████| 27.7M/27.7M [00:08<00:00, 3.44MB/s] 135 | Downloading CMake 3.13.5... 136 | 100%|██████████████████████████████████████| 30.6M/30.6M [00:08<00:00, 3.82MB/s] 137 | Downloading CMake 3.14.7... 138 | 100%|██████████████████████████████████████| 32.0M/32.0M [00:08<00:00, 4.04MB/s] 139 | Downloading CMake 3.15.7... 140 | 100%|██████████████████████████████████████| 33.2M/33.2M [00:10<00:00, 3.44MB/s] 141 | Downloading CMake 3.16.5... 142 | 100%|██████████████████████████████████████| 34.2M/34.2M [00:08<00:00, 4.11MB/s] 143 | Downloading CMake 3.17.0... 144 | 100%|██████████████████████████████████████| 35.3M/35.3M [00:10<00:00, 3.67MB/s] 145 | ``` 146 | 147 | The script downloads and unpacks different versions of CMake into the `tools` folder. 148 | 149 | ## License 150 | 151 | 152 | 153 | The code is licensed under the [MIT License](http://opensource.org/licenses/MIT): 154 | 155 | Copyright © 2020-2024 [Niels Lohmann](http://nlohmann.me) 156 | 157 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 158 | 159 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 160 | 161 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 162 | -------------------------------------------------------------------------------- /cmake_downloader.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import platform 5 | import re 6 | import tarfile 7 | import tempfile 8 | import zipfile 9 | from pathlib import Path 10 | from typing import Dict, List 11 | 12 | import requests 13 | from packaging.version import parse as version_parse 14 | from tqdm import tqdm 15 | 16 | TIMEOUT_SECONDS = 10 17 | session = requests.Session() 18 | 19 | 20 | def get_folders() -> List[str]: 21 | html = session.get(url="https://cmake.org/files/", timeout=TIMEOUT_SECONDS).text 22 | return list(re.findall(r">v([0-9.]+)", html)) 23 | 24 | 25 | def get_tarball_urls_version(base_version: str) -> List[str]: 26 | url = f"https://cmake.org/files/v{base_version}/" 27 | html = session.get(url=url, timeout=TIMEOUT_SECONDS).text 28 | return sorted([url + filename for filename in re.findall(r">(cmake-[0-9rc.]+-[^.]+(?:\.tar\.gz|\.zip))", html)]) 29 | 30 | 31 | def get_tarball_urls() -> List[str]: 32 | folders = get_folders() 33 | result = [] # type: List[str] 34 | 35 | print("Retrieving URLs...") 36 | for folder in tqdm(folders): 37 | urls = get_tarball_urls_version(folder) 38 | result += urls 39 | 40 | return result 41 | 42 | 43 | def download_and_extract(url: str, path: Path) -> None: 44 | # derive file directory name from URL 45 | file_name_start_pos = url.rfind("/") + 1 46 | file_name = url[file_name_start_pos:] 47 | file_wo_ext = file_name.replace(".tar.gz", "").replace(".zip", "") 48 | 49 | if not (path / file_wo_ext).exists(): 50 | response = session.get(url=url, timeout=TIMEOUT_SECONDS, stream=True) 51 | response.raise_for_status() 52 | file_size = int(response.headers["Content-Length"]) 53 | 54 | tmpdir = tempfile.TemporaryDirectory() 55 | full_file_name = Path(tmpdir.name) / file_name 56 | 57 | progress = tqdm(total=file_size, unit="B", unit_scale=True, unit_divisor=1024) 58 | with Path(full_file_name).open(mode="wb+") as f: 59 | for data in response: 60 | progress.update(len(data)) 61 | f.write(data) 62 | progress.close() 63 | 64 | if url.endswith(".zip"): 65 | with zipfile.ZipFile(full_file_name, mode="r") as zip_ref: 66 | zip_ref.extractall(path) 67 | else: 68 | with tarfile.open(full_file_name, mode="r:gz") as tar: 69 | tar.extractall(path=path) 70 | 71 | 72 | def create_version_dict(os: str) -> Dict[str, str]: 73 | tarball_urls = get_tarball_urls() 74 | result = {} 75 | 76 | for tarball_url in tarball_urls: 77 | version = re.findall(r"cmake-(([0-9.]+)(-rc[0-9]+)?)", tarball_url)[0][0] 78 | 79 | if ( 80 | ( 81 | os == "macos" 82 | and ("Darwin64" in tarball_url or "Darwin-x86_64" in tarball_url or "macos-universal" in tarball_url) 83 | ) 84 | or (os == "linux" and ("Linux-x86_64" in tarball_url or "linux-x86_64" in tarball_url)) 85 | or ( 86 | os == "windows" 87 | and ("win32-x86" in tarball_url or "win64-x64" in tarball_url or "windows-x86_64" in tarball_url) 88 | ) 89 | ) and ( 90 | version_parse(version).public not in result 91 | or ( 92 | version_parse(version).public in result 93 | and ("win64-x64" in tarball_url or "windows-x86_64" in tarball_url) 94 | ) 95 | ): 96 | result[version_parse(version).public] = tarball_url 97 | 98 | return result 99 | 100 | 101 | if __name__ == "__main__": 102 | # get default value for current system 103 | default_os = ( 104 | "macos" 105 | if platform.system() == "Darwin" 106 | else "linux" if platform.system() == "Linux" else "windows" if platform.system() == "Windows" else None 107 | ) 108 | 109 | parser = argparse.ArgumentParser(description="Download CMake binaries.") 110 | parser.add_argument( 111 | "--os", 112 | help=f"OS to download CMake for (default: {default_os})", 113 | choices=["macos", "linux", "windows"], 114 | default=default_os, 115 | ) 116 | parser.add_argument( 117 | "--latest_release", 118 | action="store_true", 119 | help="only download the latest release (default: False)", 120 | ) 121 | parser.add_argument( 122 | "--latest_patch", 123 | action="store_true", 124 | help="only download the latest patch version for each release (default: False)", 125 | ) 126 | parser.add_argument( 127 | "--first_minor", 128 | action="store_true", 129 | help="only download the first minor version for each release (default: False)", 130 | ) 131 | parser.add_argument( 132 | "--release_candidates", 133 | action="store_true", 134 | help="also consider release candidates (default: False)", 135 | ) 136 | parser.add_argument("--min_version", help="only download versions greater or equal than MIN_VERSION") 137 | parser.add_argument("--max_version", help="only download versions less or equal than MAX_VERSION") 138 | parser.add_argument( 139 | "--tools_directory", 140 | metavar="DIR", 141 | default="tools", 142 | help='path to the CMake binaries (default: "tools")', 143 | ) 144 | args = parser.parse_args() 145 | 146 | version_dict = create_version_dict(os=args.os) 147 | versions = sorted([version_parse(version) for version in version_dict]) 148 | print(f"Found {len(versions)} versions from {versions[0]} to {versions[-1]}.") 149 | 150 | if args.min_version: 151 | versions = sorted([version for version in versions if version >= version_parse(args.min_version)]) 152 | 153 | if args.max_version: 154 | versions = sorted([version for version in versions if version <= version_parse(args.max_version)]) 155 | 156 | if not args.release_candidates: 157 | versions = [version for version in versions if not version.is_prerelease] 158 | 159 | if args.latest_patch: 160 | result = [] 161 | for major, minor in {(version.major, version.minor) for version in versions}: 162 | result.append([version for version in versions if version.major == major and version.minor == minor][-1]) 163 | versions = sorted(result) 164 | 165 | if args.first_minor: 166 | result = [] 167 | for major, minor in {(version.major, version.minor) for version in versions}: 168 | result.append(next(version for version in versions if version.major == major and version.minor == minor)) 169 | versions = sorted(result) 170 | 171 | if args.latest_release: 172 | versions = versions[-1:] 173 | 174 | for idx, version in enumerate(versions): 175 | print(f"Downloading CMake {version.public} ({idx+1}/{len(versions)})...") 176 | download_and_extract(url=version_dict[version.public], path=Path(args.tools_directory)) 177 | -------------------------------------------------------------------------------- /cmake_min_version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import contextlib 5 | import math 6 | import platform 7 | import re 8 | import subprocess 9 | import sys 10 | import tempfile 11 | from pathlib import Path 12 | from time import time 13 | from typing import List, NamedTuple, Optional 14 | 15 | from packaging.version import parse as version_parse 16 | from termcolor import colored 17 | 18 | 19 | class CMakeBinary(NamedTuple): 20 | version: str 21 | binary: Path 22 | 23 | 24 | class ConfigureResult: 25 | def __init__(self, return_code: int, stderr: str): 26 | self.success = return_code == 0 # type: bool 27 | self.proposed_version = None # type: Optional[str] 28 | self.reason = None # type: Optional[str] 29 | self.stderr = stderr 30 | 31 | # try to read proposed minimal version from stderr output 32 | try: 33 | self.proposed_version = re.findall(r"CMake ([^ ]+) or higher is required.", stderr)[0] 34 | 35 | # support ranges 36 | if ".." in self.proposed_version: 37 | self.proposed_version = self.proposed_version.split("..")[0] 38 | # make sure all versions are major.minor.patch 39 | if self.proposed_version.count(".") == 1: 40 | self.proposed_version += ".0" 41 | except IndexError: 42 | pass 43 | 44 | try: 45 | self.reason = re.findall(r"CMake Error at (.*):", stderr)[0] 46 | except IndexError: 47 | try: 48 | self.reason = re.findall(r"CMake Error: ([^\n]+)", stderr)[0] 49 | except IndexError: 50 | pass 51 | 52 | 53 | def get_cmake_binaries(tools_dir: Path) -> List[CMakeBinary]: 54 | start_time = time() 55 | binaries = [] # type: List[CMakeBinary] 56 | if platform.system() == "Windows": 57 | filenames = tools_dir.rglob("**/bin/cmake.exe") 58 | else: 59 | filenames = tools_dir.rglob("**/bin/cmake") 60 | 61 | for filename in filenames: 62 | with contextlib.suppress(IndexError): 63 | version = re.findall(r"cmake-([^-]+)-", str(filename))[0] 64 | binaries.append(CMakeBinary(version, Path(filename).resolve())) 65 | 66 | print(f"Found {len(binaries)} CMake binaries from directory {tools_dir} in {time()-start_time:.2f} seconds\n") 67 | return sorted(binaries, key=lambda x: version_parse(x.version)) 68 | 69 | 70 | def try_configure(binary: Path, cmake_parameters: List[str]) -> ConfigureResult: 71 | tmpdir = tempfile.TemporaryDirectory() 72 | proc = subprocess.Popen( 73 | [binary, *cmake_parameters, "-Wno-dev"], 74 | stdout=subprocess.DEVNULL, 75 | stderr=subprocess.PIPE, 76 | cwd=tmpdir.name, 77 | ) 78 | proc.wait() 79 | 80 | return ConfigureResult( 81 | return_code=proc.returncode, 82 | stderr=proc.stderr.read().decode("utf-8") if proc.stderr else "", 83 | ) 84 | 85 | 86 | def binary_search(*, cmake_parameters: List[str], tools_dir: Path, error_output: bool) -> Optional[CMakeBinary]: 87 | versions = get_cmake_binaries(tools_dir) # type: List[CMakeBinary] 88 | cmake_versions = [len(cmake.version) for cmake in versions] 89 | if len(cmake_versions) == 0: 90 | print( 91 | colored( 92 | "Error: No CMake versions found in the tool dir. Make sure to run the cmake_downloader script first.", 93 | "red", 94 | ), 95 | ) 96 | sys.exit(1) 97 | longest_version_string = max(cmake_versions) + 1 # type: int 98 | 99 | lower_idx = 0 # type: int 100 | upper_idx = len(versions) - 1 # type: int 101 | last_success_idx = None # type: Optional[int] 102 | 103 | steps = 0 # type: int 104 | 105 | while lower_idx <= upper_idx: 106 | mid_idx = int((lower_idx + upper_idx) / 2) # type: int 107 | cmake_binary = versions[mid_idx] # type: CMakeBinary 108 | 109 | steps += 1 110 | remaining_versions = upper_idx - lower_idx + 1 # type: int 111 | remaining_steps = int(math.ceil(math.log2(remaining_versions))) # type: int 112 | 113 | print( 114 | "[{progress:3.0f}%] CMake {cmake_version:{longest_version_string}}".format( 115 | progress=100.0 * float(steps - 1) / (steps + remaining_steps), 116 | cmake_version=cmake_binary.version, 117 | longest_version_string=longest_version_string, 118 | ), 119 | end="", 120 | flush=True, 121 | ) 122 | 123 | result = try_configure(binary=cmake_binary.binary, cmake_parameters=cmake_parameters) # type: ConfigureResult 124 | 125 | if result.success: 126 | print(colored("✔ works", "green")) 127 | last_success_idx = mid_idx 128 | upper_idx = mid_idx - 1 129 | else: 130 | print(colored("✘ error", "red")) 131 | if error_output: 132 | for line in result.stderr.splitlines(): 133 | print(colored(f" {line}", "yellow")) 134 | elif result.reason: 135 | print(colored(f" {result.reason}", "yellow")) 136 | proposed_binary = [x for x in versions if x.version == result.proposed_version] 137 | lower_idx = versions.index(proposed_binary[0]) if len(proposed_binary) else mid_idx + 1 138 | 139 | return versions[last_success_idx] if last_success_idx is not None else None 140 | 141 | 142 | def full_search(*, cmake_parameters: List[str], tools_dir: Path, error_output: bool) -> Optional[CMakeBinary]: 143 | versions = get_cmake_binaries(tools_dir) # type: List[CMakeBinary] 144 | longest_version_string = max([len(cmake.version) for cmake in versions]) + 1 # type: int 145 | last_success_idx = None # type: Optional[int] 146 | 147 | for steps, cmake_binary in enumerate(versions): 148 | print( 149 | "[{progress:3.0f}%] CMake {cmake_version:{longest_version_string}}".format( 150 | progress=100.0 * float(steps) / len(versions), 151 | cmake_version=cmake_binary.version, 152 | longest_version_string=longest_version_string, 153 | ), 154 | end="", 155 | flush=True, 156 | ) 157 | 158 | result = try_configure(binary=cmake_binary.binary, cmake_parameters=cmake_parameters) # type: ConfigureResult 159 | 160 | if result.success: 161 | print(colored("✔ works", "green")) 162 | if not last_success_idx: 163 | last_success_idx = steps 164 | else: 165 | last_success_idx = None 166 | print(colored("✘ error", "red")) 167 | if error_output: 168 | for line in result.stderr.splitlines(): 169 | print(colored(f" {line}", "yellow")) 170 | elif result.reason: 171 | print(colored(f" {result.reason}", "yellow")) 172 | 173 | return versions[last_success_idx] if last_success_idx is not None else None 174 | 175 | 176 | if __name__ == "__main__": 177 | parser = argparse.ArgumentParser(description="Find the minimal required CMake version for a project.") 178 | parser.add_argument("params", type=str, nargs="+", help="parameters to pass to CMake") 179 | parser.add_argument( 180 | "--tools_directory", 181 | metavar="DIR", 182 | default="tools", 183 | help='path to the CMake binaries (default: "tools")', 184 | ) 185 | parser.add_argument( 186 | "--full_search", 187 | default=False, 188 | action="store_true", 189 | help="Searches using a top down approach instead of a binary search (default: False)", 190 | ) 191 | parser.add_argument( 192 | "--error_details", 193 | default=False, 194 | action="store_true", 195 | help="Print the full stderr output in case of an error (default: False)", 196 | ) 197 | args = parser.parse_args() 198 | 199 | if args.full_search: 200 | working_version = full_search( 201 | cmake_parameters=args.params, 202 | tools_dir=Path(args.tools_directory), 203 | error_output=args.error_details, 204 | ) 205 | else: 206 | working_version = binary_search( 207 | cmake_parameters=args.params, 208 | tools_dir=Path(args.tools_directory), 209 | error_output=args.error_details, 210 | ) 211 | 212 | if working_version: 213 | print( 214 | "[100%] Minimal working version: {cmake} {version}".format( 215 | cmake=colored("CMake", "blue"), 216 | version=colored(working_version.version, "blue"), 217 | ), 218 | ) 219 | 220 | print(f"\ncmake_minimum_required(VERSION {working_version.version})") 221 | 222 | else: 223 | print("[100%] {message}".format(message=colored("ERROR: Could not find working version.", "red"))) 224 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | 3 | line-length = 120 4 | target-version = ["py37", "py38", "py39", "py310", "py311", "py312"] 5 | 6 | [tool.isort] 7 | 8 | profile = "black" 9 | 10 | [tool.ruff] 11 | 12 | line-length = 120 13 | indent-width = 4 14 | target-version = "py37" 15 | 16 | [tool.ruff.lint] 17 | 18 | select = ["ALL"] 19 | ignore = [ 20 | "D", 21 | "FA100", 22 | "ANN101", 23 | "ANN204", 24 | "T201" 25 | ] 26 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | 3 | ruff==0.3.5 4 | black==24.3.0 5 | isort==5.13.2 6 | 7 | mypy==1.9.0 8 | types-requests==2.31.0.20240402 9 | types-tqdm==4.66.0.20240106 10 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm==4.66.2 2 | requests==2.31.0 3 | packaging==24.0 4 | termcolor==2.3.0 # 2.4.0 breaks Python 3.7 support 5 | --------------------------------------------------------------------------------