diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 0000000..3994ec0 --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true)$ +ref-names: $Format:%D$ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..a94cb2f --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +.git_archival.txt export-subst diff --git a/.gitignore b/.gitignore index a720064..6850808 100644 --- a/.gitignore +++ b/.gitignore @@ -64,4 +64,8 @@ target/ # IDE .idea/ +# Virtual Environments venv/ + +# Build Artifacts +dronecan_gui_tool/_version_generated.py diff --git a/dronecan_gui_tool/main.py b/dronecan_gui_tool/main.py index 32b1650..18bc95d 100644 --- a/dronecan_gui_tool/main.py +++ b/dronecan_gui_tool/main.py @@ -42,12 +42,23 @@ logging.basicConfig(stream=sys.stderr, level=logging_level, format='%(asctime)s %(levelname)s %(name)s %(message)s') - -from .version import __version__ +try: + from .version import __version_tuple__ +except ModuleNotFoundError: + __version_tuple__ = (0, 0, 0, "unknown") +__version__ = [x for x in __version_tuple__ if isinstance(x, int)] if args.version: - v = '.'.join(map(str, __version__)) + metadata_parts = [x for x in __version_tuple__ if isinstance(x, str)] + is_clean_release = len(metadata_parts) == 0 + is_dirty = any(".d" in part for part in metadata_parts) + version_info = '.'.join(map(str, __version__)) + metadata_info = '.'.join(map(str, metadata_parts)) print("DroneCAN GUI Tool is an application for DroneCAN bus management and diagnostics") - print(f"DroneCAN GUI Tool Version: {v}") + print(f"DroneCAN GUI Tool Version: {version_info}") + if not is_clean_release: + print(f"Development Build: {metadata_info}") + if is_dirty: + print("Warning: Built from a dirty working tree!") sys.exit(0) log_file = tempfile.NamedTemporaryFile(mode='w', prefix='dronecan_gui_tool-', suffix='.log', delete=False) diff --git a/dronecan_gui_tool/version.py b/dronecan_gui_tool/version.py index c29544a..33d4fc7 100644 --- a/dronecan_gui_tool/version.py +++ b/dronecan_gui_tool/version.py @@ -8,7 +8,99 @@ # Andrew Tridgell # # -__version__ = 1, 2, 28 +import os +import subprocess +import re +# Note: This version is determined dynamically at build time or runtime. +__version_tuple__ = None +# 1. Try running git describe first (live git repository state) +try: + + git_describe = subprocess.check_output( + ["git", "describe", "--tags", "--long", "--dirty"], + stderr=subprocess.DEVNULL, + text=True + ).strip() + is_dirty = git_describe.endswith("-dirty") + if is_dirty: + git_describe = git_describe[:-6] # slice off the '-dirty' + + # Split from the right, because the tag itself might contain hyphens (e.g., v1.0-beta) + parts = git_describe.rsplit("-", 2) + + if len(parts) == 3: + base_tag = parts[0] + commits_since = int(parts[1]) + sha = parts[2] + + # Parse base tag into tuple (e.g., "1.2.28" -> (1, 2, 28)) + # Remove leading 'v' if present + if base_tag.startswith('v'): + base_tag = base_tag[1:] + + tag_parts = tuple(int(x) for x in base_tag.split('.') if x.isdigit()) + + __version_tuple__ = tag_parts + if commits_since > 0: + __version_tuple__ += (f"source-post{commits_since}", sha) + if is_dirty: + __version_tuple__ += ("dirty", ) +except (FileNotFoundError, subprocess.CalledProcessError): + # 2. Try to import the generated version information (built wheels/MSIs) + try: + from ._version_generated import __version_tuple__ + except ImportError: + # 3. Fall back to reading .git_archival.txt (GitHub source ZIPs) + __version_tuple__ = (0, 0, 0, "unknown") + + archival_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".git_archival.txt") + if os.path.isfile(archival_path): + with open(archival_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Extract node hash + node_match = re.search(r'^node:[ \t]*([a-f0-9]{40})$', content, re.MULTILINE) + # Extract describe-name + describe_match = re.search(r'^describe-name:[ \t]*(.+)$', content, re.MULTILINE) + # Extract ref-names + ref_match = re.search(r'^ref-names:[ \t]*(.+)$', content, re.MULTILINE) + + if node_match and not node_match.group(1).startswith('$'): + sha = 'g' + node_match.group(1)[:7] + base_tag = None + commits_since = 0 + + if describe_match and not describe_match.group(1).startswith('$'): + describe_str = describe_match.group(1).strip() + parts = describe_str.rsplit("-", 2) + + if len(parts) == 3 and parts[1].isdigit() and parts[2].startswith('g'): + base_tag = parts[0] + commits_since = int(parts[1]) + else: + base_tag = describe_str + + if not base_tag and ref_match and not ref_match.group(1).startswith('$'): + refs = ref_match.group(1).split(', ') + tags = [r[5:] for r in refs if r.startswith('tag: ')] + if tags: + base_tag = tags[0] + + if base_tag: + if base_tag.startswith('v'): + base_tag = base_tag[1:] + + tag_parts = tuple(int(x) for x in base_tag.split('.') if x.isdigit()) + if tag_parts: + __version_tuple__ = tag_parts + if commits_since > 0: + __version_tuple__ += (f"source-post{commits_since}", sha) + else: + __version_tuple__ = (0, 0, 0, "source", sha) + else: + __version_tuple__ = (0, 0, 0, "source", sha) + else: + print("Warning: Git is not available and .git_archival.txt not found") diff --git a/dronecan_gui_tool/widgets/about_window.py b/dronecan_gui_tool/widgets/about_window.py index 4ae739a..a31c5db 100644 --- a/dronecan_gui_tool/widgets/about_window.py +++ b/dronecan_gui_tool/widgets/about_window.py @@ -7,21 +7,32 @@ # import dronecan -from ..version import __version__ +try: + from ..version import __version_tuple__ +except ImportError: + __version_tuple__ = (0, 0, 0, "unknown") from . import get_icon, get_app_icon from PyQt6.QtWidgets import QDialog, QTableWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, \ QTableWidgetItem, QHeaderView from PyQt6.QtGui import QIcon from PyQt6.QtCore import Qt, PYQT_VERSION_STR, QSize +numeric_parts = [x for x in __version_tuple__ if isinstance(x, int)] +metadata_parts = [x for x in __version_tuple__ if isinstance(x, str)] +is_clean_release = len(metadata_parts) == 0 +is_dirty = any(".d" in part for part in metadata_parts) +version_info = '.'.join(map(str, numeric_parts)) +metadata_info = '.'.join(map(str, metadata_parts)) -ABOUT_TEXT = (''' -
Cross-platform application for DroneCAN bus management and diagnostics.
+ +This application is distributed under the terms of the MIT software license. The source repository and the bug \ +tracker are located at https://github.com/DroneCAN/gui_tool.
+""") def _list_3rd_party(): diff --git a/pyproject.toml b/pyproject.toml index cb8aec3..374c675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,5 +2,5 @@ # you need to drop setup_requires from setup.py as well. # https://peps.python.org/pep-0518/#rationale [build-system] -requires = ["setuptools>=42",'setuptools_git>=1.0'] +requires = ["setuptools>=42",'setuptools_git>=1.0',"setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index 7650984..2f9095f 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,6 @@ upgrade_code = '{D5CD6E19-2545-32C7-A62A-4595B28BCDC3}' sys.path.append(os.path.join(SOURCE_DIR, PACKAGE_NAME)) -from version import __version__ assert sys.version_info[0] == 3, 'Python 3 is required' @@ -36,7 +35,10 @@ # args = dict( name=PACKAGE_NAME, - version='.'.join(map(str, __version__)), + use_scm_version={ + "write_to": "dronecan_gui_tool/_version_generated.py", + "version_scheme": "post-release" + }, packages=find_packages(), install_requires=[ 'setuptools>=18.5',