-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathversion.py
More file actions
319 lines (262 loc) · 11.7 KB
/
Copy pathversion.py
File metadata and controls
319 lines (262 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
#!/usr/bin/env python3
"""
Version management for Cosmos Collection
Handles version information from local fallback and GitHub releases
"""
import requests
import json
import logging
import subprocess
import sys
import os
from typing import Optional, Dict, Any
from datetime import datetime, timedelta
# Set up logging
logger = logging.getLogger(__name__)
# Local fallback version - updated automatically during GitHub Actions build
_FALLBACK_VERSION = "1.0.10"
_FALLBACK_BUILD_DATE = "2025-09-22"
def _get_git_version() -> Optional[str]:
"""
Try to get version from git tags when running from source.
Returns None if not in a git repository or git is not available.
"""
# Get script directory (needed for both main and fallback methods)
script_dir = os.path.dirname(os.path.abspath(__file__))
# Check if .git directory exists
if not os.path.exists(os.path.join(script_dir, '.git')):
return None
# Windows-specific flag to avoid console window popup
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
try:
# Try faster command first: get most recent tag
result = subprocess.run(
['git', 'describe', '--tags', '--abbrev=0'],
capture_output=True,
text=True,
timeout=5, # Increased timeout for Windows
cwd=script_dir,
creationflags=creation_flags
)
if result.returncode == 0:
version = result.stdout.strip().lstrip('v')
if version:
logger.debug(f"Detected git version: {version}")
return version
except subprocess.TimeoutExpired:
logger.debug("Git describe timed out, trying alternative method")
# Fallback: try getting tag from current commit or most recent tag
try:
result = subprocess.run(
['git', 'tag', '--points-at', 'HEAD'],
capture_output=True,
text=True,
timeout=3,
cwd=script_dir,
creationflags=creation_flags
)
if result.returncode == 0 and result.stdout.strip():
version = result.stdout.strip().split('\n')[0].lstrip('v')
logger.debug(f"Detected git version from tag: {version}")
return version
except Exception:
pass
except (FileNotFoundError, Exception) as e:
logger.debug(f"Could not get git version: {e}")
return None
def _get_git_commit_date() -> Optional[str]:
"""
Try to get the date of the latest commit when running from source.
Returns None if not in a git repository or git is not available.
"""
script_dir = os.path.dirname(os.path.abspath(__file__))
if not os.path.exists(os.path.join(script_dir, '.git')):
return None
# Windows-specific flag to avoid console window popup
creation_flags = subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0
try:
result = subprocess.run(
['git', 'log', '-1', '--format=%ci'],
capture_output=True,
text=True,
timeout=5, # Increased timeout for Windows consistency
cwd=script_dir,
creationflags=creation_flags
)
if result.returncode == 0:
# Parse date from git format (YYYY-MM-DD HH:MM:SS +0000)
date_str = result.stdout.strip().split()[0]
if date_str:
logger.debug(f"Detected git commit date: {date_str}")
return date_str
except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e:
logger.debug(f"Could not get git commit date: {e}")
return None
def _is_packaged_build() -> bool:
"""Check if running as a packaged/frozen build (e.g., PyInstaller)"""
return getattr(sys, 'frozen', False)
# Determine version and build date
# For packaged builds: use fallback (set by CI during build)
# For source builds: try to use git, fallback to hardcoded if git unavailable
if _is_packaged_build():
__version__ = _FALLBACK_VERSION
__build_date__ = _FALLBACK_BUILD_DATE
logger.debug("Using packaged build version")
else:
__version__ = _get_git_version() or _FALLBACK_VERSION
__build_date__ = _get_git_commit_date() or _FALLBACK_BUILD_DATE
if __version__ != _FALLBACK_VERSION:
logger.debug(f"Using git-detected version: {__version__}")
else:
logger.debug("Using fallback version (git not available)")
class VersionManager:
"""Manages version information for the application"""
def __init__(self):
self.github_repo = "quake101/CosmosCollection"
self.cache_duration = timedelta(hours=6) # Cache GitHub API response for 6 hours
self._cached_release_info = None
self._cache_timestamp = None
def get_local_version(self) -> str:
"""Get the local fallback version"""
return __version__
def get_build_date(self) -> str:
"""Get the build date"""
return __build_date__
def get_github_latest_release(self) -> Optional[Dict[Any, Any]]:
"""
Fetch the latest release information from GitHub API
Returns None if unable to fetch or if cached data is still valid
"""
try:
# Check if we have valid cached data
if (self._cached_release_info and self._cache_timestamp and
datetime.now() - self._cache_timestamp < self.cache_duration):
return self._cached_release_info
# Fetch from GitHub API
url = f"https://api.github.com/repos/{self.github_repo}/releases/latest"
headers = {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'CosmosCollection'
}
# Disable SSL verification for PyInstaller builds
import sys
verify_ssl = not getattr(sys, 'frozen', False)
response = requests.get(url, headers=headers, timeout=10, verify=verify_ssl)
response.raise_for_status()
release_data = response.json()
# Cache the response
self._cached_release_info = release_data
self._cache_timestamp = datetime.now()
logger.debug(f"Fetched latest release: {release_data.get('tag_name', 'Unknown')}")
return release_data
except requests.exceptions.RequestException as e:
logger.debug(f"Could not fetch GitHub release info: {e}")
return None
except json.JSONDecodeError as e:
logger.debug(f"Could not parse GitHub API response: {e}")
return None
except Exception as e:
logger.debug(f"Unexpected error fetching release info: {e}")
return None
def get_version_info(self) -> Dict[str, Any]:
"""
Get comprehensive version information
Returns a dictionary with version details
"""
version_info = {
'local_version': self.get_local_version(),
'build_date': self.get_build_date(),
'github_available': False,
'github_version': None,
'github_url': None,
'github_published_date': None,
'is_latest': None,
'update_available': False
}
# Try to get GitHub release info
github_release = self.get_github_latest_release()
if github_release:
version_info['github_available'] = True
version_info['github_version'] = github_release.get('tag_name', 'Unknown')
version_info['github_url'] = github_release.get('html_url')
# Parse published date
published_at = github_release.get('published_at')
if published_at:
try:
pub_date = datetime.fromisoformat(published_at.replace('Z', '+00:00'))
version_info['github_published_date'] = pub_date.strftime('%Y-%m-%d')
except:
version_info['github_published_date'] = published_at
# Compare versions
local_version = self.get_local_version()
github_version = version_info['github_version']
if github_version and local_version:
version_info['is_latest'] = self._compare_versions(local_version, github_version)
version_info['update_available'] = not version_info['is_latest']
return version_info
def _compare_versions(self, local_version: str, github_version: str) -> bool:
"""
Compare local version with GitHub version
Returns True if local version is the same or newer, False if update available
"""
try:
# Remove 'v' prefix if present
local_clean = local_version.lstrip('v').strip()
github_clean = github_version.lstrip('v').strip()
# Split into parts and compare
local_parts = [int(x) for x in local_clean.split('.')]
github_parts = [int(x) for x in github_clean.split('.')]
# Pad shorter version with zeros
max_len = max(len(local_parts), len(github_parts))
local_parts.extend([0] * (max_len - len(local_parts)))
github_parts.extend([0] * (max_len - len(github_parts)))
# Compare versions
for local_part, github_part in zip(local_parts, github_parts):
if local_part < github_part:
return False # Update available
elif local_part > github_part:
return True # Local is newer
return True # Versions are equal
except (ValueError, AttributeError) as e:
logger.debug(f"Error comparing versions {local_version} vs {github_version}: {e}")
return True # Assume no update needed if comparison fails
def get_version_display_string(self) -> str:
"""Get a formatted version string for display in the UI"""
version_info = self.get_version_info()
if version_info['github_available'] and version_info['github_version']:
if version_info['update_available']:
return f"v{version_info['local_version']} (Update available: {version_info['github_version']})"
else:
return f"v{version_info['local_version']} (Latest)"
else:
return f"v{version_info['local_version']}"
def get_detailed_version_info(self) -> str:
"""Get detailed version information for the About dialog"""
version_info = self.get_version_info()
details = [f"Local Version: {version_info['local_version']}"]
if version_info['github_available']:
if version_info['update_available']:
details.append(f"Latest Release: {version_info['github_version']} (Update Available)")
else:
details.append(f"Latest Release: {version_info['github_version']} ✓")
if version_info['github_published_date']:
details.append(f"Release Date: {version_info['github_published_date']}")
else:
details.append("GitHub status: Offline")
return "\n".join(details)
# Global instance for easy access
version_manager = VersionManager()
# Convenience functions
def get_version() -> str:
"""Get the current application version"""
return version_manager.get_local_version()
def get_version_info() -> Dict[str, Any]:
"""Get comprehensive version information"""
return version_manager.get_version_info()
def get_version_display() -> str:
"""Get formatted version string for UI display"""
return version_manager.get_version_display_string()
def check_for_updates() -> bool:
"""Check if updates are available"""
version_info = version_manager.get_version_info()
return version_info.get('update_available', False)