From 88f9d453bae76242b27c0e4fb79f361b3b8332fd Mon Sep 17 00:00:00 2001 From: James O'SHANNESSY <12959316+joshanne@users.noreply.github.com> Date: Fri, 24 Jul 2026 17:00:24 +1000 Subject: [PATCH 1/4] app: Simplify and improve the version reporting Because the version is only incremented prior to a release, we have a number of commits between releases that all identify as the last release. This means, all of our most recent changes that have improved gui_tool look like it's an old version, and managing users installs is hard. 'But it works on v1.2.28' - when it's actually a dev build the user is running... --- dronecan_gui_tool/main.py | 19 +++++++++--- dronecan_gui_tool/version.py | 37 ++++++++++++++++++++++- dronecan_gui_tool/widgets/about_window.py | 25 ++++++++++----- pyproject.toml | 2 +- setup.py | 3 +- 5 files changed, 71 insertions(+), 15 deletions(-) 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..463adc5 100644 --- a/dronecan_gui_tool/version.py +++ b/dronecan_gui_tool/version.py @@ -8,7 +8,42 @@ # Andrew Tridgell # # -__version__ = 1, 2, 28 +# Note: This version is incremented after the release has been made +__version_tuple__ = 1, 2, 28 + + +# Now try to import the generated version information and override the locally managed version info + +try: + from ._version_generated import __version_tuple__ +except ImportError: + try: + import subprocess + 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] + __version_tuple__ += (f"source-dev{commits_since}", sha) + if is_dirty: + __version_tuple__ += ("dirty", ) + except (FileNotFoundError, subprocess.CalledProcessError): + # FileNotFoundError: The 'git' executable is not installed/available + # CalledProcessError: The command failed (e.g., not inside a git repository) + print("Warning: Git is not available") + __version_tuple__ += ("source",) 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 = (''' -

DroneCAN GUI Tool v{0}

-Cross-platform application for DroneCAN bus management and diagnostics. +ABOUT_TEXT = (f""" +

DroneCAN GUI Tool v{version_info}

+{f"

Dev Build {'(Dirty)' if is_dirty else ''}: {metadata_info}

" if not is_clean_release else ""} -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. -'''.format('.'.join(map(str, __version__)))).strip().replace('\n', '\n
') +

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..b1c9ca6 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,7 @@ # args = dict( name=PACKAGE_NAME, - version='.'.join(map(str, __version__)), + use_scm_version={"write_to": "dronecan_gui_tool/_version_generated.py"}, packages=find_packages(), install_requires=[ 'setuptools>=18.5', From fb2f58d3a30ac82eb6bedadd626729373e21f465 Mon Sep 17 00:00:00 2001 From: James O'SHANNESSY <12959316+joshanne@users.noreply.github.com> Date: Sat, 25 Jul 2026 10:37:44 +1000 Subject: [PATCH 2/4] git: Ignore the generated version info --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) 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 From 2ba5daf3f3230c26ecfa3d176782498402493541 Mon Sep 17 00:00:00 2001 From: James O'SHANNESSY <12959316+joshanne@users.noreply.github.com> Date: Sat, 25 Jul 2026 14:21:48 +1000 Subject: [PATCH 3/4] app: better extract version number of build --- .git_archival.txt | 4 ++ .gitattributes | 1 + dronecan_gui_tool/version.py | 78 +++++++++++++++++++++++++++++++----- setup.py | 5 ++- 4 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 .git_archival.txt create mode 100644 .gitattributes 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/dronecan_gui_tool/version.py b/dronecan_gui_tool/version.py index 463adc5..a95eadd 100644 --- a/dronecan_gui_tool/version.py +++ b/dronecan_gui_tool/version.py @@ -9,17 +9,19 @@ # # -# Note: This version is incremented after the release has been made -__version_tuple__ = 1, 2, 28 - +# Note: This version is determined dynamically at build time or runtime. +__version_tuple__ = None # Now try to import the generated version information and override the locally managed version info - try: from ._version_generated import __version_tuple__ except ImportError: + import os + import subprocess + import re + + # Try running git describe first try: - import subprocess git_describe = subprocess.check_output( ["git", "describe", "--tags", "--long", "--dirty"], stderr=subprocess.DEVNULL, @@ -37,13 +39,69 @@ base_tag = parts[0] commits_since = int(parts[1]) sha = parts[2] - __version_tuple__ += (f"source-dev{commits_since}", sha) + + # 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): - # FileNotFoundError: The 'git' executable is not installed/available - # CalledProcessError: The command failed (e.g., not inside a git repository) - print("Warning: Git is not available") - __version_tuple__ += ("source",) + # We are likely running from a downloaded GitHub archive where .git is missing. + # Fall back to reading .git_archival.txt + __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/setup.py b/setup.py index b1c9ca6..2f9095f 100755 --- a/setup.py +++ b/setup.py @@ -35,7 +35,10 @@ # args = dict( name=PACKAGE_NAME, - use_scm_version={"write_to": "dronecan_gui_tool/_version_generated.py"}, + use_scm_version={ + "write_to": "dronecan_gui_tool/_version_generated.py", + "version_scheme": "post-release" + }, packages=find_packages(), install_requires=[ 'setuptools>=18.5', From 69793c2b6c34e0687c37749e3b9bfd2941679ef6 Mon Sep 17 00:00:00 2001 From: James O'SHANNESSY <12959316+joshanne@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:01:11 +1000 Subject: [PATCH 4/4] app: invert logic when pulling in version info. Precedence for version information: * git describe * setuptools_scm generated file * .git_archival.txt * default value --- dronecan_gui_tool/version.py | 79 ++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/dronecan_gui_tool/version.py b/dronecan_gui_tool/version.py index a95eadd..33d4fc7 100644 --- a/dronecan_gui_tool/version.py +++ b/dronecan_gui_tool/version.py @@ -9,55 +9,54 @@ # # +import os +import subprocess +import re + # Note: This version is determined dynamically at build time or runtime. __version_tuple__ = None -# Now try to import the generated version information and override the locally managed version info +# 1. Try running git describe first (live git repository state) try: - from ._version_generated import __version_tuple__ -except ImportError: - import os - import subprocess - import re - - # Try running git describe first - 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' + 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) + # 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): - # We are likely running from a downloaded GitHub archive where .git is missing. - # Fall back to reading .git_archival.txt + 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') + 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()