🔧
CMakeのFecthContentを自動で更新する
TL;DR
- CMakeで依存関係を自動的にダウンロードできるけど、自動更新するツールはないよ!
- CMakeLists.txtを解析して自動バージョンアップを実装したよ!
はじめに
C/C++ではコンパイラに依存しないビルドツールとしてCMakeが広く使われています。また、CMakeを使えば依存関係をきちんと記述して、ライブラリを簡単にリンクできます。
CMakeにはfetchContent
という仕組みがあり、CMakeLists.txt
に依存関係を記述し、依存するライブラリを自動的にダウンロードできます。しかし、CMakeのfetchContent
は他のプログラミング言語のように中央集権的なリポジトリがないため、自動で更新する便利なツールはなく、GIT_TAG
オプションで指定するGitのコミットやタグを書き換えるくらいしかできません。
また、リポジトリごとにコミットやタグとリリースされたバージョンの関連も異なるため、更新するときの課題となります。
そこで、CMakeLists.txtを解析して自動で更新するPythonスクリプトを書きました。
具体的には、依存関係を抽出し、そのgithubやgitlabなどのリポジトリのAPIを叩き、最新のリリースに対応するタグを取得してCMakeLists.txtを更新するライブラリを作りました。
コード
import re
import requests
from dataclasses import dataclass
@dataclass
class CMakeDependency:
name: str
repo: str
tag: str
def __latest_from_gh(self, repo_name) -> str:
url_release = f"https://api.github.com/repos/{repo_name}/releases/latest"
response_release = requests.get(url_release)
if response_release.status_code == 200:
result_release = response_release.json()
return result_release["tag_name"]
else:
# No releases found, get the default branch
url_repo = f"https://api.github.com/repos/{repo_name}"
response_repo = requests.get(url_repo)
response_repo.raise_for_status()
default_branch = response_repo.json()["default_branch"]
url_default_branch = (
f"https://api.github.com/repos/{repo_name}/branches/{default_branch}"
)
response_default_branch = requests.get(url_default_branch)
response_default_branch.raise_for_status()
return response_default_branch.json()["commit"]["sha"]
def __latest_from_gl(self, repo_name) -> str:
encoded_repo = repo_name.replace("/", "%2F")
release_url = f"https://gitlab.com/api/v4/projects/{encoded_repo}/releases"
response_release = requests.get(release_url)
if response_release.status_code == 200:
result = response_release.json()
return result[0]["tag_name"]
elif response_release.status_code == 404:
# No releases found, get the default branch
repo_url = f"https://gitlab.com/{encoded_repo}"
response_repo = requests.get(repo_url)
response_repo.raise_for_status()
default_branch = response_repo.json()["default_branch"]
default_branch_url = f"https://gitlab.com/api/v4/projects/{encoded_repo}/repository/branches/{default_branch}"
response_default_branch = requests.get(default_branch_url)
response_default_branch.raise_for_status()
return response_default_branch.json()["commit"]["id"]
else:
response_release.raise_for_status()
def latest(self) -> str:
gh_pattern = r"https://github.com/(\S+).git"
gl_pattern = r"https://gitlab.com/(\S+).git"
if re.match(gh_pattern, self.repo):
repo_name = re.sub(gh_pattern, r"\1", self.repo)
return self.__latest_from_gh(repo_name)
elif re.match(gl_pattern, self.repo):
repo_name = re.sub(gl_pattern, r"\1", self.repo)
return self.__latest_from_gl(repo_name)
else:
raise ValueError(f"Unknown repository: {self.repo}")
if __name__ == "__main__":
print("Checking CMake dependencies...")
with open("CMakeLists.txt", "r+") as f:
cmake_src = f.read()
fetched_deps_pattern = r"^FetchContent_Declare\(\s*(\S+)\s*GIT_REPOSITORY\s+(\S+)\s+GIT_TAG\s+(\S+)\s*\)"
fetched_deps = re.findall(fetched_deps_pattern, cmake_src, re.MULTILINE)
fetched_deps = [
CMakeDependency(name, repo, tag) for name, repo, tag in fetched_deps
]
for dep in fetched_deps:
latest = dep.latest()
if dep.tag == latest:
continue
print(f"Updating {dep.name} ({dep.repo}): {dep.tag} -> {latest}")
dep_old_pattern = f"FetchContent_Declare\\(\\s*{dep.name}\\s*GIT_REPOSITORY\\s+{dep.repo}\\s+GIT_TAG\\s+{dep.tag}\\s*\\)"
dep_old = re.findall(dep_old_pattern, cmake_src, re.MULTILINE)
if len(dep_old) == 0:
raise ValueError(f"Dependency {dep.name} not found in CMakeLists.txt")
elif len(dep_old) > 1:
raise ValueError(
f"Multiple definitions of {dep.name} found in CMakeLists.txt"
)
else:
dep_old = dep_old[0]
dep_new = dep_old.replace(dep.tag, latest)
cmake_src = re.sub(dep_old_pattern, dep_new, cmake_src)
f.seek(0)
f.write(cmake_src)
f.truncate()
print("All CMake dependencies updated.")
このスクリプトは以下の構造となっています。
- 依存関係の記述
FetchContent_Declare
を正規表現を使って検索し、、各依存ライブラリの名前、リポジトリURL、タグを抽出してCMakeDependency
クラスのインスタンスにまとめます。 - リポジトリのURLに基づき、GitHubの場合は/releases/latestエンドポイントから最新リリースのタグを、リリース情報がない場合はデフォルトブランチの最新コミットを取得します。GitLabの場合も同様にAPIで最新リリース情報またはデフォルトブランチの最新コミットIDを取得しています。
- 各依存ライブラリについて、現在指定されているタグと最新のタグを比較し、異なる場合は古いタグを最新のものに置換してファイルを更新します。
おわりに
CMakeで依存関係を記述するfetchContent
を使った依存関係を自動的に更新するスクリプトを紹介しました。このスクリプトを実行するだけで、依存関係を更新できるので、人力の更新作業を削減できます。
現状ではgithubとgitlabに対応しており、全ての依存関係を最新に更新しますが、今後は他のリポジトリへの対応や更新対象を限定するオプションの追加を検討しています。
では、よきCMakeライフを!
Discussion