From 5b3de3007baa4d9b258c172a305d0b2c67b1d55c Mon Sep 17 00:00:00 2001 From: lucocozz Date: Sat, 26 Apr 2025 23:07:36 +0200 Subject: [PATCH] packaging: update conan and vcpkg --- .gitignore | 3 +- packaging/conan/conandata.yml | 9 +- packaging/conan/conanfile.py | 23 +- packaging/conan/test_package/CMakeLists.txt | 4 +- packaging/conan/test_package/conanfile.py | 4 +- packaging/conan/test_package/test_package.c | 2 +- packaging/vcpkg/libcargs.json | 9 - packaging/vcpkg/lucocozz-cargs.json | 9 + packaging/vcpkg/portfile.cmake | 2 +- packaging/vcpkg/vcpkg.json | 4 +- tools/version.py | 82 +++ tools/version_core.py | 539 ++++++++++++++++++++ update_version.sh | 389 -------------- 13 files changed, 658 insertions(+), 421 deletions(-) delete mode 100644 packaging/vcpkg/libcargs.json create mode 100644 packaging/vcpkg/lucocozz-cargs.json create mode 100644 tools/version.py create mode 100644 tools/version_core.py delete mode 100755 update_version.sh diff --git a/.gitignore b/.gitignore index eb57637..bd736fd 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,5 @@ a.out reports/ .venv/ packaging/conan/test_package/build/ -packaging/conan/test_package/CMakeUserPresets.json \ No newline at end of file +packaging/conan/test_package/CMakeUserPresets.json +*.pyc \ No newline at end of file diff --git a/packaging/conan/conandata.yml b/packaging/conan/conandata.yml index cb6e6ba..99dfbfd 100644 --- a/packaging/conan/conandata.yml +++ b/packaging/conan/conandata.yml @@ -1,7 +1,4 @@ sources: - "1.0.1": - url: "https://github.com/lucocozz/cargs/archive/refs/tags/v1.0.1.tar.gz" - sha256: "d709ab4d3d338693a1ba5c3e685fa5ee2d2ef37bb92a1038c03a11f2d6d3dcd8" - "1.0.0": - url: "https://github.com/lucocozz/cargs/archive/refs/tags/v1.0.0.tar.gz" - sha256: "387315fe54f7ce51f4c7176fce4e65e4876a58893a8e516ad1d58209136cde19" + "1.0.2": + url: "https://github.com/lucocozz/cargs/archive/refs/tags/v1.0.2.tar.gz" + sha256: "15c64c271a109030538e3cb88a6fade49021a05f99bc372edd40fba2ec4cf0b5" diff --git a/packaging/conan/conanfile.py b/packaging/conan/conanfile.py index 8ff3eef..58eb1eb 100644 --- a/packaging/conan/conanfile.py +++ b/packaging/conan/conanfile.py @@ -4,11 +4,11 @@ from conan.tools.layout import basic_layout import os -class LibcargsConan(ConanFile): - name = "libcargs" - version = "1.0.1" +class LucocozzCargsConan(ConanFile): + name = "lucocozz-cargs" + version = "1.0.2" description = "Modern C library for command-line argument parsing with an elegant, macro-based API" - topics = ("conan", "cargs", "libcargs", "command-line", "arguments", "parser", "cli", "argparse") + topics = ("conan", "cargs", "lucocozz-cargs", "command-line", "arguments", "parser", "cli", "argparse") url = "https://github.com/lucocozz/cargs" homepage = "https://github.com/lucocozz/cargs" license = "MIT" @@ -39,7 +39,7 @@ def layout(self): def requirements(self): if not self.options.disable_regex: - self.requires("pcre2/10.42") + self.requires("pcre2/[>=10.40]") def build_requirements(self): self.tool_requires("meson/[>=1.0.0]") @@ -51,7 +51,7 @@ def source(self): def generate(self): tc = MesonToolchain(self) tc.project_options["buildtype"] = str(self.settings.build_type).lower() - tc.project_options["disable_regex"] = self.options.disable_regex + tc.project_options["disable_regex"] = bool(self.options.disable_regex) tc.project_options["tests"] = False tc.project_options["examples"] = False tc.project_options["benchmarks"] = False @@ -70,12 +70,19 @@ def package(self): meson.install() def package_info(self): - self.cpp_info.set_property("cmake_file_name", "libcargs") - self.cpp_info.set_property("cmake_target_name", "libcargs::libcargs") + self.cpp_info.set_property("cmake_file_name", "lucocozz-cargs") + self.cpp_info.set_property("cmake_target_name", "lucocozz-cargs::lucocozz-cargs") self.cpp_info.libs = ["cargs"] + + if not self.options.disable_regex: + # Make sure PCRE2 is linked correctly + self.cpp_info.requires = ["pcre2::pcre2"] + if self.options.disable_regex: self.cpp_info.defines.append("CARGS_NO_REGEX") + if self.settings.os == "Linux": self.cpp_info.system_libs.append("m") + self.cpp_info.set_property("cmake_find_mode", "both") self.cpp_info.set_property("pkg_config_name", "cargs") diff --git a/packaging/conan/test_package/CMakeLists.txt b/packaging/conan/test_package/CMakeLists.txt index 25b9eb0..482953b 100644 --- a/packaging/conan/test_package/CMakeLists.txt +++ b/packaging/conan/test_package/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.15) project(test_package C) -find_package(libcargs REQUIRED) +find_package(lucocozz-cargs REQUIRED) add_executable(test_package test_package.c) -target_link_libraries(test_package libcargs::libcargs) +target_link_libraries(test_package lucocozz-cargs::lucocozz-cargs) diff --git a/packaging/conan/test_package/conanfile.py b/packaging/conan/test_package/conanfile.py index b204bfe..ce4cb9a 100644 --- a/packaging/conan/test_package/conanfile.py +++ b/packaging/conan/test_package/conanfile.py @@ -3,7 +3,7 @@ from conan.tools.build import can_run from conan.tools.cmake import CMake, cmake_layout -class LibcargsTestConan(ConanFile): +class LucocozzCargsTestConan(ConanFile): settings = "os", "compiler", "build_type", "arch" generators = "CMakeDeps", "CMakeToolchain" @@ -22,5 +22,5 @@ def test(self): if self.settings.os == "Windows": return if can_run(self): - cmd = os.path.join(self.cpp.build.bindir, "test_package") + cmd = os.path.join(self.build_folder, "test_package") self.run(f"{cmd} -v", env="conanrun") diff --git a/packaging/conan/test_package/test_package.c b/packaging/conan/test_package/test_package.c index 1cedf03..93c5099 100644 --- a/packaging/conan/test_package/test_package.c +++ b/packaging/conan/test_package/test_package.c @@ -23,7 +23,7 @@ int main(int argc, char **argv) printf("Test package ran successfully!\n"); printf(" Verbose: %s\n", verbose ? "yes" : "no"); printf(" Output: %s\n", output); - + cargs_free(&cargs); return 0; } diff --git a/packaging/vcpkg/libcargs.json b/packaging/vcpkg/libcargs.json deleted file mode 100644 index 13b5430..0000000 --- a/packaging/vcpkg/libcargs.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "versions": [ - { - "git-tree": "9c4f49c55663f06e265f6dcedd82e17f990ddba8", - "version": "1.0.1", - "port-version": 0 - } - ] -} diff --git a/packaging/vcpkg/lucocozz-cargs.json b/packaging/vcpkg/lucocozz-cargs.json new file mode 100644 index 0000000..759a78d --- /dev/null +++ b/packaging/vcpkg/lucocozz-cargs.json @@ -0,0 +1,9 @@ +{ + "versions": [ + { + "git-tree": "2b016c569e19d960bd2d26aefe695c69c57192a8", + "version": "1.0.2", + "port-version": 0 + } + ] +} diff --git a/packaging/vcpkg/portfile.cmake b/packaging/vcpkg/portfile.cmake index 19aa02e..064d53d 100644 --- a/packaging/vcpkg/portfile.cmake +++ b/packaging/vcpkg/portfile.cmake @@ -2,7 +2,7 @@ vcpkg_from_github( OUT_SOURCE_PATH SOURCE_PATH REPO lucocozz/cargs REF v${VERSION} - SHA512 74bb7eebb1f328d3b27c51c2cec74f87b0a11fd7c801bb3d2d4b56b8ed7448461043f8f01b68f69cee67c2c73217ebb8b641a5e76a632052910ddb13b750045f + SHA512 40780e98a72fa225bdde62d45b349f558bfd32171f65393fdd8eb0da1566cb0c0083adbea620452a8bf8ff960898e216674612b94b2a556b07498a8fd7dd10d3 HEAD_REF main ) diff --git a/packaging/vcpkg/vcpkg.json b/packaging/vcpkg/vcpkg.json index 3a4af0d..d1fecf1 100644 --- a/packaging/vcpkg/vcpkg.json +++ b/packaging/vcpkg/vcpkg.json @@ -1,6 +1,6 @@ { - "name": "libcargs", - "version": "1.0.1", + "name": "lucocozz-cargs", + "version": "1.0.2", "description": "Modern C library for command-line argument parsing with an elegant, macro-based API", "homepage": "https://github.com/lucocozz/cargs", "license": "MIT", diff --git a/tools/version.py b/tools/version.py new file mode 100644 index 0000000..17385fd --- /dev/null +++ b/tools/version.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +""" +version.py - Modern version management for the cargs library +""" + +import argparse +import os +import sys +from typing import Optional + +from version_core import Version, VersionBump, VersionManager + + +def main(): + """Parse command line arguments and execute requested actions""" + parser = argparse.ArgumentParser(description="cargs version management tool") + + parser.add_argument("--dry-run", action="store_true", help="Show what would be done without making changes") + parser.add_argument("-v", "--verbose", action="store_true", help="Show detailed output") + + subparsers = parser.add_subparsers(dest="command", required=True) + + get_parser = subparsers.add_parser("get", help="Get the current version") + + bump_parser = subparsers.add_parser("bump", help="Bump the version") + bump_parser.add_argument("type", choices=["major", "minor", "patch"], help="Type of version bump") + + set_parser = subparsers.add_parser("set", help="Set a specific version") + set_parser.add_argument("version", help="New version (X.Y.Z)") + + release_parser = subparsers.add_parser("release", help="Perform a complete release") + version_group = release_parser.add_mutually_exclusive_group(required=True) + version_group.add_argument("--bump", choices=["major", "minor", "patch"], help="Bump version by major, minor, or patch") + version_group.add_argument("--version", help="Set specific version (X.Y.Z)") + + release_parser.add_argument("--skip-tag", action="store_true", help="Skip creating git tag") + release_parser.add_argument("--skip-packages", action="store_true", help="Skip updating package managers") + release_parser.add_argument("--skip-changelog", action="store_true", help="Skip updating changelog") + release_parser.add_argument("--skip-github-release", action="store_true", help="Skip creating GitHub release") + release_parser.add_argument("--conan-path", help="Path to conan-center-index repository") + release_parser.add_argument("--vcpkg-path", help="Path to vcpkg repository") + + args = parser.parse_args() + + # Initialize version manager + version_manager = VersionManager(dry_run=args.dry_run, verbose=args.verbose) + + # Execute command + if args.command == "get": + print(f"Current version: {version_manager.current_version}") + + elif args.command == "bump": + bump_type = VersionBump(args.type) + new_version = version_manager.current_version.bump(bump_type) + version_manager.update_version(new_version) + + elif args.command == "set": + new_version = Version.from_string(args.version) + version_manager.update_version(new_version) + + elif args.command == "release": + # Determine the new version + if args.version: + new_version = Version.from_string(args.version) + else: + bump_type = VersionBump(args.bump) + new_version = None + + version_manager.release( + new_version=new_version, + bump_type=bump_type if args.bump else None, + skip_tag=args.skip_tag, + skip_packages=args.skip_packages, + skip_changelog=args.skip_changelog, + skip_github_release=args.skip_github_release, + conan_index_path=args.conan_path, + vcpkg_path=args.vcpkg_path + ) + + +if __name__ == "__main__": + main() diff --git a/tools/version_core.py b/tools/version_core.py new file mode 100644 index 0000000..d75dda9 --- /dev/null +++ b/tools/version_core.py @@ -0,0 +1,539 @@ +#!/usr/bin/env python3 +"""Core implementation of version management functionality""" + +import hashlib +import os +import re +import shutil +import subprocess +import tempfile +import time +from dataclasses import dataclass +from enum import Enum +from pathlib import Path +from typing import Dict, List, Optional, Tuple + + +class VersionBump(Enum): + MAJOR = "major" + MINOR = "minor" + PATCH = "patch" + + +@dataclass +class Version: + major: int + minor: int + patch: int + + @classmethod + def from_string(cls, version_str: str) -> "Version": + match = re.match(r"(\d+)\.(\d+)\.(\d+)", version_str) + if not match: + raise ValueError(f"Invalid version format: {version_str}") + return cls( + major=int(match.group(1)), + minor=int(match.group(2)), + patch=int(match.group(3)) + ) + + def bump(self, bump_type: VersionBump) -> "Version": + if bump_type == VersionBump.MAJOR: + return Version(self.major + 1, 0, 0) + elif bump_type == VersionBump.MINOR: + return Version(self.major, self.minor + 1, 0) + elif bump_type == VersionBump.PATCH: + return Version(self.major, self.minor, self.patch + 1) + else: + raise ValueError(f"Unknown bump type: {bump_type}") + + def __str__(self) -> str: + return f"{self.major}.{self.minor}.{self.patch}" + + +class VersionManager: + def __init__(self, dry_run: bool = False, verbose: bool = False): + self.dry_run = dry_run + self.verbose = verbose + self.repo_root = self._get_repo_root() + self.current_version = self._get_current_version() + self.files_to_update = self._collect_files_to_update() + + def _get_repo_root(self) -> Path: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=True, + ) + return Path(result.stdout.strip()) + + def _run_command(self, cmd: List[str], cwd: Optional[Path] = None) -> str: + if self.verbose: + print(f"Running: {' '.join(cmd)}") + + if self.dry_run and not cmd[0] in ["git", "grep"]: + return "DRY-RUN" + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + cwd=cwd or self.repo_root, + check=True, + ) + return result.stdout.strip() + + def _get_current_version(self) -> Version: + meson_build_path = self.repo_root / "meson.build" + with open(meson_build_path, "r") as f: + content = f.read() + + match = re.search(r"version: '(\d+\.\d+\.\d+)'", content) + if not match: + raise ValueError("Could not find version in meson.build") + + return Version.from_string(match.group(1)) + + def _collect_files_to_update(self) -> Dict[str, List[Tuple[str, str]]]: + file_patterns = { + "meson.build": [ + (r"version: '(\d+\.\d+\.\d+)'", "version: '{}'"), + ], + "includes/cargs.h": [ + (r'#define CARGS_VERSION "(\d+\.\d+\.\d+)"', '#define CARGS_VERSION "{}"'), + ], + "packaging/conan/conanfile.py": [ + (r'version = "(\d+\.\d+\.\d+)"', 'version = "{}"'), + ], + "packaging/vcpkg/vcpkg.json": [ + (r'"version": "(\d+\.\d+\.\d+)"', '"version": "{}"'), + ], + "packaging/installers/install.sh": [ + (r'VERSION="(\d+\.\d+\.\d+)"', 'VERSION="{}"'), + ], + "README.md": [ + (r'conan install libcargs/(\d+\.\d+\.\d+)@', 'conan install libcargs/{}@'), + (r'curl -LO https://github.com/lucocozz/cargs/releases/download/v(\d+\.\d+\.\d+)/cargs-', + 'curl -LO https://github.com/lucocozz/cargs/releases/download/v{}/cargs-'), + ], + } + + result = {} + for file_path, patterns in file_patterns.items(): + full_path = self.repo_root / file_path + if full_path.exists(): + result[str(full_path)] = patterns + + return result + + def update_version(self, new_version: Version) -> None: + print(f"Updating version from {self.current_version} to {new_version}") + + for file_path, patterns in self.files_to_update.items(): + if self.verbose: + print(f"Updating {file_path}") + + with open(file_path, "r") as f: + content = f.read() + + for pattern, replacement in patterns: + replacement_formatted = replacement.format(new_version) + content = re.sub(pattern, replacement_formatted, content) + + if not self.dry_run: + with open(file_path, "w") as f: + f.write(content) + + if not self.dry_run: + self._run_command(["git", "add", "."]) + self._run_command(["git", "commit", "-m", f"Bump version to {new_version}"]) + print(f"✅ Changes committed with message: 'Bump version to {new_version}'") + + def create_tag(self, version: Version) -> None: + tag_name = f"v{version}" + + self._run_command(["git", "tag", "-a", tag_name, "-m", f"Version {version}"]) + self._run_command(["git", "push", "origin", tag_name]) + + print(f"✅ Created and pushed tag: {tag_name}") + + def calculate_archive_hash(self, version: Version) -> str: + if not self.dry_run: + print("Waiting for GitHub to process the new tag...") + time.sleep(5) + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + archive_name = f"v{version}.tar.gz" + archive_path = temp_path / archive_name + archive_url = f"https://github.com/lucocozz/cargs/archive/refs/tags/{archive_name}" + + if not self.dry_run: + max_attempts = 5 + for attempt in range(1, max_attempts + 1): + try: + self._run_command(["curl", "-sL", archive_url, "-o", str(archive_path)]) + + if archive_path.exists() and archive_path.stat().st_size > 0: + break + except subprocess.CalledProcessError: + pass + + print(f"Attempt {attempt} failed, retrying in 3 seconds...") + time.sleep(3) + + if not archive_path.exists() or archive_path.stat().st_size == 0: + raise RuntimeError("Failed to download archive after multiple attempts") + + sha512 = hashlib.sha512() + with open(archive_path, "rb") as f: + for chunk in iter(lambda: f.read(4096), b""): + sha512.update(chunk) + return sha512.hexdigest() + else: + return "DRY_RUN_SAMPLE_HASH" + + def update_hashes(self, version: Version, sha512: str) -> None: + # Update hash in conandata.yml + conandata_path = self.repo_root / "packaging/conan/conandata.yml" + if conandata_path.exists(): + with open(conandata_path, "r") as f: + content = f.read() + + # Check if version already exists + version_pattern = f'"{version}":' + if version_pattern in content: + # Update existing entry + content = re.sub( + f'{version_pattern}.*?sha256: ".*?"', + f'{version_pattern}\n url: "https://github.com/lucocozz/cargs/archive/refs/tags/v{version}.tar.gz"\n sha256: "{sha512}"', + content, + flags=re.DOTALL + ) + else: + # Add new version entry + content = re.sub( + r'sources:', + f'sources:\n "{version}":\n url: "https://github.com/lucocozz/cargs/archive/refs/tags/v{version}.tar.gz"\n sha256: "{sha512}"', + content + ) + + if not self.dry_run: + with open(conandata_path, "w") as f: + f.write(content) + + # Update hash in vcpkg portfile + portfile_path = self.repo_root / "packaging/vcpkg/portfile.cmake" + if portfile_path.exists(): + with open(portfile_path, "r") as f: + content = f.read() + + # Update SHA512 hash + content = re.sub( + r'SHA512 [a-fA-F0-9]*', + f'SHA512 {sha512}', + content + ) + + # Update REF if needed + if "REF v${VERSION}" not in content: + content = re.sub( + r'REF v[0-9.]+', + f'REF v{version}', + content + ) + + if not self.dry_run: + with open(portfile_path, "w") as f: + f.write(content) + + # Commit hash updates + if not self.dry_run: + self._run_command(["git", "add", "."]) + self._run_command(["git", "commit", "-m", f"Update SHA512 hash for version {version}"]) + self._run_command(["git", "push"]) + print(f"✅ Hash updates committed and pushed") + + def update_changelog(self, version: Version) -> None: + changelog_path = self.repo_root / "CHANGELOG.md" + + if not changelog_path.exists(): + if self.verbose: + print("Creating new CHANGELOG.md file") + + # Create new changelog file + changelog_content = f"""# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [{version}] - {time.strftime("%Y-%m-%d")} + +### Added +- [Add your new features here] + +### Changed +- [Add your changes here] + +### Fixed +- [Add your bug fixes here] + +""" + if not self.dry_run: + with open(changelog_path, "w") as f: + f.write(changelog_content) + else: + with open(changelog_path, "r") as f: + content = f.read() + + # Check if version already exists + if f"## [{version}]" in content: + print(f"Version {version} already exists in CHANGELOG.md") + return + + # Add new version entry + content = re.sub( + r"# Changelog.*?## \[", + f"# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [{version}] - {time.strftime('%Y-%m-%d')}\n\n### Added\n- [Add your new features here]\n\n### Changed\n- [Add your changes here]\n\n### Fixed\n- [Add your bug fixes here]\n\n## [", + content, + count=1, + flags=re.DOTALL + ) + + if not self.dry_run: + with open(changelog_path, "w") as f: + f.write(content) + + print(f"✅ Updated CHANGELOG.md with version {version}") + + if not self.dry_run: + self._run_command(["git", "add", "CHANGELOG.md"]) + self._run_command(["git", "commit", "--amend", "--no-edit"]) + print("✅ Added CHANGELOG.md to version commit") + + def create_github_release(self, version: Version, prerelease: bool = False) -> None: + try: + self._run_command(["gh", "--version"]) + except (subprocess.CalledProcessError, FileNotFoundError): + print("⚠️ GitHub CLI not installed. Please install it to create GitHub releases.") + return + + try: + self._run_command(["gh", "auth", "status"]) + except subprocess.CalledProcessError: + print("⚠️ Not authenticated with GitHub CLI. Please run 'gh auth login'") + return + + cmd = ["gh", "release", "create", f"v{version}"] + cmd.extend(["--title", f"Version {version}"]) + + changelog_path = self.repo_root / "CHANGELOG.md" + if changelog_path.exists(): + with open(changelog_path, "r") as f: + content = f.read() + + pattern = f"## \\[{version}\\].*?(?=## \\[|$)" + match = re.search(pattern, content, re.DOTALL) + if match: + with tempfile.NamedTemporaryFile(mode="w", delete=False) as temp: + temp.write(match.group(0)) + notes_path = temp.name + + cmd.extend(["--notes-file", notes_path]) + + if prerelease: + cmd.append("--prerelease") + + try: + self._run_command(cmd) + print(f"✅ Created GitHub release for version {version}") + except subprocess.CalledProcessError as e: + print(f"⚠️ Failed to create GitHub release: {e}") + finally: + if 'notes_path' in locals(): + os.unlink(notes_path) + + def update_conan_package(self, version, sha512: str, conan_index_path: Optional[str] = None): + if conan_index_path is None: + conan_index_path = os.path.expanduser("~/conan-center-index") + + conan_recipe_path = os.path.join(conan_index_path, "recipes/libcargs") + + if not os.path.exists(conan_recipe_path): + print(f"⚠️ Could not find Conan recipe at {conan_recipe_path}") + print(f"Please clone the conan-center-index repository or specify the correct path") + return + + conan_data_path = os.path.join(conan_recipe_path, "all/conandata.yml") + + if self.verbose: + print(f"Updating Conan package at {conan_recipe_path}") + + if not self.dry_run: + # Create a backup + shutil.copy(conan_data_path, f"{conan_data_path}.bak") + + with open(conan_data_path, "r") as f: + content = f.read() + + # Check if version already exists + version_pattern = f'"{version}":' + if version_pattern in content: + content = re.sub( + f'{version_pattern}.*?sha256: ".*?"', + f'{version_pattern}\n url: "https://github.com/lucocozz/cargs/archive/refs/tags/v{version}.tar.gz"\n sha256: "{sha512}"', + content, + flags=re.DOTALL + ) + else: + content = re.sub( + r'sources:', + f'sources:\n "{version}":\n url: "https://github.com/lucocozz/cargs/archive/refs/tags/v{version}.tar.gz"\n sha256: "{sha512}"', + content + ) + + with open(conan_data_path, "w") as f: + f.write(content) + + # Update conanfile.py if needed + conanfile_path = os.path.join(conan_recipe_path, "all/conanfile.py") + if os.path.exists(conanfile_path): + with open(conanfile_path, "r") as f: + content = f.read() + + if re.search(r'name = "libcargs"', content): + content = re.sub( + r'version = "[0-9.]+"', + f'version = "{version}"', + content + ) + + with open(conanfile_path, "w") as f: + f.write(content) + + branch_name = f"libcargs-{version}" + print("\n" + "="*80) + print(f"✅ Conan package updated successfully") + print("Next steps for creating a Conan PR:") + print(f"1. cd {conan_index_path}") + print(f"2. git checkout -b {branch_name}") + print(f"3. git add .") + print(f'4. git commit -m "libcargs: update to {version}"') + print(f"5. git push -u origin {branch_name}") + print(f"6. Create a pull request on GitHub") + print("="*80 + "\n") + + def update_vcpkg_package(self, version, sha512: str, vcpkg_path: Optional[str] = None): + if vcpkg_path is None: + vcpkg_path = os.path.expanduser("~/vcpkg") + + vcpkg_port_path = os.path.join(vcpkg_path, "ports/libcargs") + + if not os.path.exists(vcpkg_port_path): + print(f"⚠️ Could not find vcpkg port at {vcpkg_port_path}") + print(f"Please clone the vcpkg repository or specify the correct path") + return + + portfile_path = os.path.join(vcpkg_port_path, "portfile.cmake") + + if self.verbose: + print(f"Updating vcpkg port at {vcpkg_port_path}") + + if not self.dry_run: + # Create a backup + shutil.copy(portfile_path, f"{portfile_path}.bak") + + with open(portfile_path, "r") as f: + content = f.read() + + # Update SHA512 hash + content = re.sub( + r'SHA512 [a-fA-F0-9]*', + f'SHA512 {sha512}', + content + ) + + # Update the version + if "REF v${VERSION}" in content: + vcpkg_json_path = os.path.join(vcpkg_port_path, "vcpkg.json") + if os.path.exists(vcpkg_json_path): + with open(vcpkg_json_path, "r") as f: + json_content = f.read() + + json_content = re.sub( + r'"version": "[0-9.]+"', + f'"version": "{version}"', + json_content + ) + + with open(vcpkg_json_path, "w") as f: + f.write(json_content) + else: + content = re.sub( + r'REF v[0-9.]+', + f'REF v{version}', + content + ) + + with open(portfile_path, "w") as f: + f.write(content) + + branch_name = f"libcargs-{version}" + print("\n" + "="*80) + print(f"✅ vcpkg port updated successfully") + print("Next steps for creating a vcpkg PR:") + print(f"1. cd {vcpkg_path}") + print(f"2. git checkout -b {branch_name}") + print(f"3. git add .") + print(f'4. git commit -m "[libcargs] Update to version {version}"') + print(f"5. git push -u origin {branch_name}") + print(f"6. Create a pull request on GitHub") + print("="*80 + "\n") + + def release(self, new_version: Optional[Version] = None, + bump_type: Optional[VersionBump] = None, + skip_tag: bool = False, skip_packages: bool = False, + skip_changelog: bool = False, + skip_github_release: bool = False, + conan_index_path: Optional[str] = None, + vcpkg_path: Optional[str] = None) -> None: + # Determine the new version + if new_version is None and bump_type is None: + raise ValueError("Either new_version or bump_type must be specified") + + if new_version is None: + new_version = self.current_version.bump(bump_type) + + # Update version in all files + self.update_version(new_version) + + # Update changelog + if not skip_changelog: + self.update_changelog(new_version) + + # Create git tag + if not skip_tag: + self.create_tag(new_version) + + # Calculate archive hash + sha512 = self.calculate_archive_hash(new_version) + + # Update hash values + self.update_hashes(new_version, sha512) + + # Update package managers + if not skip_packages: + self.update_conan_package(new_version, sha512, conan_index_path) + self.update_vcpkg_package(new_version, sha512, vcpkg_path) + + # Create GitHub release + if not skip_github_release and not skip_tag: + self.create_github_release(new_version) + + # Final message + print("\n" + "="*80) + print(f"🎉 Version {new_version} released successfully!") + print("="*80) diff --git a/update_version.sh b/update_version.sh deleted file mode 100755 index 6dd5a2d..0000000 --- a/update_version.sh +++ /dev/null @@ -1,389 +0,0 @@ -#!/bin/bash -set -e - -# ANSI color codes -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -BLUE='\033[0;34m' -BOLD='\033[1m' -NC='\033[0m' # No Color - -# Default paths (can be overridden by environment variables) -CONAN_INDEX_PATH="${CONAN_INDEX_PATH:-$HOME/conan-center-index}" -VCPKG_PATH="${VCPKG_PATH:-$HOME/vcpkg}" -REPO_PATH="$(pwd)" - -# Print usage information -show_help() { - echo -e "${BLUE}${BOLD}cargs Version Updater${NC}" - echo "This script automates the version update process for cargs." - echo "" - echo "Usage: $0 NEW_VERSION [options]" - echo "" - echo "Arguments:" - echo " NEW_VERSION New version number (e.g., 1.1.0)" - echo "" - echo "Options:" - echo " --current=VERSION Current version number (default: auto-detect)" - echo " --conan-path=PATH Path to conan-center-index repo (default: $CONAN_INDEX_PATH)" - echo " --vcpkg-path=PATH Path to vcpkg repo (default: $VCPKG_PATH)" - echo " --no-conan Skip Conan package update" - echo " --no-vcpkg Skip vcpkg package update" - echo " --no-tag Don't create Git tag" - echo " --dry-run Show what would be done without making changes" - echo " --help Show this help message" - echo "" - echo "Example: $0 1.1.0" -} - -# Parse command-line arguments -NEW_VERSION="" -CURRENT_VERSION="" -SKIP_CONAN=false -SKIP_VCPKG=false -SKIP_TAG=false -DRY_RUN=false - -while [ "$#" -gt 0 ]; do - case "$1" in - --help) - show_help - exit 0 - ;; - --current=*) - CURRENT_VERSION="${1#*=}" - shift - ;; - --conan-path=*) - CONAN_INDEX_PATH="${1#*=}" - shift - ;; - --vcpkg-path=*) - VCPKG_PATH="${1#*=}" - shift - ;; - --no-conan) - SKIP_CONAN=true - shift - ;; - --no-vcpkg) - SKIP_VCPKG=true - shift - ;; - --no-tag) - SKIP_TAG=true - shift - ;; - --dry-run) - DRY_RUN=true - shift - ;; - -*) - echo -e "${RED}Error: Unknown option $1${NC}" - show_help - exit 1 - ;; - *) - if [ -z "$NEW_VERSION" ]; then - NEW_VERSION="$1" - else - echo -e "${RED}Error: Unexpected argument $1${NC}" - show_help - exit 1 - fi - shift - ;; - esac -done - -# Validate required arguments -if [ -z "$NEW_VERSION" ]; then - echo -e "${RED}Error: New version number is required${NC}" - show_help - exit 1 -fi - -# Validate version format -if ! [[ "$NEW_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo -e "${RED}Error: Version number must be in format X.Y.Z${NC}" - exit 1 -fi - -# Auto-detect current version if not specified -if [ -z "$CURRENT_VERSION" ]; then - # Try to detect from meson.build - CURRENT_VERSION=$(grep "version: '[0-9.]*'," "meson.build" | grep -Eo "[0-9]+\.[0-9]+\.[0-9]+" | head -1) - - if [ -z "$CURRENT_VERSION" ]; then - echo -e "${RED}Error: Could not auto-detect current version. Please specify with --current=VERSION${NC}" - exit 1 - fi -fi - -echo -e "${BLUE}${BOLD}cargs Version Update${NC}" -echo -e "Current version: ${YELLOW}$CURRENT_VERSION${NC}" -echo -e "New version: ${GREEN}$NEW_VERSION${NC}" -echo "" - -# Ensure repository is clean -if [ "$DRY_RUN" = false ]; then - if [ -n "$(git status --porcelain)" ]; then - echo -e "${RED}Error: Working directory is not clean. Please commit or stash changes.${NC}" - exit 1 - fi -fi - -# Function to execute or print command in dry run mode -exec_cmd() { - if [ "$DRY_RUN" = true ]; then - echo -e "${YELLOW}Would run: $*${NC}" - else - echo -e "${BLUE}Running: ${BOLD}$*${NC}" - "$@" - fi -} - -# 1. Update version strings in repository files -echo -e "${BLUE}${BOLD}Updating version in repository files...${NC}" - -# Update meson.build -exec_cmd sed -i "s/version: '$CURRENT_VERSION'/version: '$NEW_VERSION'/g" "meson.build" - -# Update installers -exec_cmd sed -i "s/VERSION=\"$CURRENT_VERSION\"/VERSION=\"$NEW_VERSION\"/g" "packaging/installers/install.sh" - -# Update packaging files -# Conan -exec_cmd sed -i "s/version = \"$CURRENT_VERSION\"/version = \"$NEW_VERSION\"/g" "packaging/conan/conanfile.py" - -# vcpkg -exec_cmd sed -i "s/\"version\": \"$CURRENT_VERSION\"/\"version\": \"$NEW_VERSION\"/g" "packaging/vcpkg/vcpkg.json" - -# 2. Commit changes -if [ "$DRY_RUN" = false ]; then - echo -e "${BLUE}${BOLD}Committing changes...${NC}" - exec_cmd git add . - exec_cmd git commit -m "Bump version to $NEW_VERSION" -else - echo -e "${YELLOW}Would commit changes with message: Bump version to $NEW_VERSION${NC}" -fi - -# 3. Create and push tag if requested -if [ "$SKIP_TAG" = false ]; then - echo -e "${BLUE}${BOLD}Creating version tag...${NC}" - if [ "$DRY_RUN" = false ]; then - exec_cmd git tag -a "v$NEW_VERSION" -m "Version $NEW_VERSION" - exec_cmd git push origin "v$NEW_VERSION" - else - echo -e "${YELLOW}Would create tag: v$NEW_VERSION${NC}" - echo -e "${YELLOW}Would push tag to origin${NC}" - fi -fi - -# 4. Download and get hash of the new tag archive -echo -e "${BLUE}${BOLD}Downloading release archive to compute hash...${NC}" -TEMP_DIR=$(mktemp -d) -ARCHIVE_URL="https://github.com/lucocozz/cargs/archive/refs/tags/v$NEW_VERSION.tar.gz" -ARCHIVE_PATH="$TEMP_DIR/v$NEW_VERSION.tar.gz" - -if [ "$DRY_RUN" = false ]; then - # Small delay to ensure GitHub has the tag available - sleep 2 - - # Try to download the archive - echo "Downloading from: $ARCHIVE_URL" - for i in {1..5}; do - if curl -sL "$ARCHIVE_URL" -o "$ARCHIVE_PATH"; then - if [ -f "$ARCHIVE_PATH" ] && [ -s "$ARCHIVE_PATH" ]; then - echo "Download successful" - break - fi - fi - echo "Attempt $i failed, retrying in 3 seconds..." - sleep 3 - done - - if [ ! -f "$ARCHIVE_PATH" ] || [ ! -s "$ARCHIVE_PATH" ]; then - echo -e "${RED}Error: Failed to download archive after multiple attempts${NC}" - echo "This might be because the tag is not yet available on GitHub." - echo "Please run the script again later or update the SHA manually." - exit 1 - fi - - # Compute hash - SHA512=$(shasum -a 512 "$ARCHIVE_PATH" | awk '{print $1}') - echo "SHA512: $SHA512" -else - echo -e "${YELLOW}Would download archive from: $ARCHIVE_URL${NC}" - echo -e "${YELLOW}Would compute SHA512 hash${NC}" - SHA512="SAMPLE_HASH_FOR_DRY_RUN" -fi - -# 5. Update package managers - -# Update Conan package -if [ "$SKIP_CONAN" = false ]; then - echo -e "${BLUE}${BOLD}Updating Conan package...${NC}" - - if [ ! -d "$CONAN_INDEX_PATH" ]; then - echo -e "${RED}Error: Conan Center Index repository not found at $CONAN_INDEX_PATH${NC}" - echo "Please clone it or specify the correct path with --conan-path" - exit 1 - fi - - # Path to recipe directory - CONAN_RECIPE_PATH="$CONAN_INDEX_PATH/recipes/libcargs" - - if [ ! -d "$CONAN_RECIPE_PATH" ]; then - echo -e "${RED}Error: libcargs recipe not found in Conan Center Index${NC}" - echo "Please ensure the recipe exists and you have the correct repository" - exit 1 - fi - - # Update conandata.yml with new version and hash - CONAN_DATA_PATH="$CONAN_RECIPE_PATH/all/conandata.yml" - - if [ "$DRY_RUN" = false ]; then - # Create a backup - cp "$CONAN_DATA_PATH" "$CONAN_DATA_PATH.bak" - - # Check if version already exists - if grep -q "\"$NEW_VERSION\":" "$CONAN_DATA_PATH"; then - # Update existing entry - sed -i "/\"$NEW_VERSION\":/,/sha256:/ s/sha256: .*/sha256: \"$SHA512\"/" "$CONAN_DATA_PATH" - else - # Add new version entry at the top - awk -v ver="$NEW_VERSION" -v hash="$SHA512" ' - /^sources:/ { - print $0; - print " \"" ver "\":"; - print " url: \"https://github.com/lucocozz/cargs/archive/refs/tags/v" ver ".tar.gz\""; - print " sha256: \"" hash "\""; - next; - } - { print $0 } - ' "$CONAN_DATA_PATH" > "$CONAN_DATA_PATH.new" - - mv "$CONAN_DATA_PATH.new" "$CONAN_DATA_PATH" - fi - - # Update conanfile.py with new version if needed - CONAN_FILE_PATH="$CONAN_RECIPE_PATH/all/conanfile.py" - if [ -f "$CONAN_FILE_PATH" ]; then - if grep -q "name = \"libcargs\"" "$CONAN_FILE_PATH"; then - sed -i "s/version = \"[0-9.]*\"/version = \"$NEW_VERSION\"/" "$CONAN_FILE_PATH" - fi - fi - - echo -e "${GREEN}Conan package updated successfully${NC}" - else - echo -e "${YELLOW}Would update Conan recipe at: $CONAN_RECIPE_PATH${NC}" - echo -e "${YELLOW}Would update conandata.yml with new version and hash${NC}" - echo -e "${YELLOW}Would update conanfile.py with new version${NC}" - fi -fi - -# Update vcpkg package -if [ "$SKIP_VCPKG" = false ]; then - echo -e "${BLUE}${BOLD}Updating vcpkg package...${NC}" - - if [ ! -d "$VCPKG_PATH" ]; then - echo -e "${RED}Error: vcpkg repository not found at $VCPKG_PATH${NC}" - echo "Please clone it or specify the correct path with --vcpkg-path" - exit 1 - fi - - # Path to port directory - VCPKG_PORT_PATH="$VCPKG_PATH/ports/libcargs" - - if [ ! -d "$VCPKG_PORT_PATH" ]; then - echo -e "${RED}Error: libcargs port not found in vcpkg${NC}" - echo "Please ensure the port exists and you have the correct repository" - exit 1 - fi - - # Update portfile.cmake with new version and hash - PORTFILE_PATH="$VCPKG_PORT_PATH/portfile.cmake" - - if [ "$DRY_RUN" = false ]; then - # Create a backup - cp "$PORTFILE_PATH" "$PORTFILE_PATH.bak" - - # Update SHA512 hash - sed -i "s/SHA512 [a-fA-F0-9]*/SHA512 $SHA512/" "$PORTFILE_PATH" - - # Update version - if grep -q "REF v\${VERSION}" "$PORTFILE_PATH"; then - # Version is referenced as a variable, check vcpkg.json - VCPKG_JSON_PATH="$VCPKG_PORT_PATH/vcpkg.json" - if [ -f "$VCPKG_JSON_PATH" ]; then - sed -i "s/\"version\": \"[0-9.]*\"/\"version\": \"$NEW_VERSION\"/" "$VCPKG_JSON_PATH" - fi - else - # Direct version reference - sed -i "s/REF v[0-9.]\+/REF v$NEW_VERSION/" "$PORTFILE_PATH" - fi - - echo -e "${GREEN}vcpkg package updated successfully${NC}" - else - echo -e "${YELLOW}Would update vcpkg port at: $VCPKG_PORT_PATH${NC}" - echo -e "${YELLOW}Would update portfile.cmake with new version and hash${NC}" - echo -e "${YELLOW}Would update vcpkg.json if needed${NC}" - fi -fi - -# Update repository with the new hash -echo -e "${BLUE}${BOLD}Updating repository with hash...${NC}" - -# Update packaging files with SHA512 hash -# Conan -exec_cmd sed -i "s/sha256: \"\"/sha256: \"$SHA512\"/" "packaging/conan/conandata.yml" - -# vcpkg -exec_cmd sed -i "s/SHA512 [a-fA-F0-9]*/SHA512 $SHA512/" "packaging/vcpkg/portfile.cmake" - -# Commit and push hash update -if [ "$DRY_RUN" = false ]; then - echo -e "${BLUE}${BOLD}Committing hash updates...${NC}" - exec_cmd git add . - exec_cmd git commit -m "Update SHA512 hash for version $NEW_VERSION" - exec_cmd git push -else - echo -e "${YELLOW}Would commit hash updates with message: Update SHA512 hash for version $NEW_VERSION${NC}" - echo -e "${YELLOW}Would push changes to origin${NC}" -fi - -# Clean up -if [ -d "$TEMP_DIR" ]; then - rm -rf "$TEMP_DIR" -fi - -if [ "$DRY_RUN" = true ]; then - echo -e "${GREEN}${BOLD}Dry run completed successfully!${NC}" - echo "Run without --dry-run to apply the changes." -else - echo -e "${GREEN}${BOLD}Version update completed successfully!${NC}" - echo -e "Updated from ${YELLOW}$CURRENT_VERSION${NC} to ${GREEN}$NEW_VERSION${NC}" - - # Print reminder to create pull requests for package managers - if [ "$SKIP_CONAN" = false ]; then - echo -e "${BLUE}Next steps for Conan:${NC}" - echo "1. cd $CONAN_INDEX_PATH" - echo "2. git checkout -b libcargs-$NEW_VERSION" - echo "3. git add ." - echo "4. git commit -m \"libcargs: update to $NEW_VERSION\"" - echo "5. git push -u origin libcargs-$NEW_VERSION" - echo "6. Create a pull request on GitHub" - fi - - if [ "$SKIP_VCPKG" = false ]; then - echo -e "${BLUE}Next steps for vcpkg:${NC}" - echo "1. cd $VCPKG_PATH" - echo "2. git checkout -b libcargs-$NEW_VERSION" - echo "3. git add ." - echo "4. git commit -m \"[libcargs] Update to version $NEW_VERSION\"" - echo "5. git push -u origin libcargs-$NEW_VERSION" - echo "6. Create a pull request on GitHub" - fi -fi