mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
Merge pull request #858 from kdmukai/version_screen_with_git_info
[Feature] Dynamic version detection; Version display screen
This commit is contained in:
@@ -18,6 +18,14 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Used by the `Version` class so it can identify the current fork
|
||||||
|
PR_AUTHOR: ${{ github.event.pull_request.user.login || github.actor }}
|
||||||
|
|
||||||
|
# Used by `Version`. We want the PR author's latest commit hash if this is a PR
|
||||||
|
# but the default SHA env var reflects a new commit into the target repo.
|
||||||
|
SOURCE_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ __pycache__/
|
|||||||
src/seedsigner.egg-info/
|
src/seedsigner.egg-info/
|
||||||
.nova
|
.nova
|
||||||
.vscode
|
.vscode
|
||||||
|
src/seedsigner/version.json
|
||||||
src/seedsigner/models/settings_definition.json
|
src/seedsigner/models/settings_definition.json
|
||||||
.idea
|
.idea
|
||||||
.coverage*
|
.coverage*
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
@@ -100,8 +102,6 @@ class Controller(Singleton):
|
|||||||
rather than at the top in order avoid circular imports.
|
rather than at the top in order avoid circular imports.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "0.8.7"
|
|
||||||
|
|
||||||
# Declare class member vars with type hints to enable richer IDE support throughout
|
# Declare class member vars with type hints to enable richer IDE support throughout
|
||||||
# the code.
|
# the code.
|
||||||
_storage: SeedStorage = None # TODO: Rename "storage" to something more indicative of its temp, in-memory state
|
_storage: SeedStorage = None # TODO: Rename "storage" to something more indicative of its temp, in-memory state
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import datetime
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -314,6 +315,110 @@ class DonateScreen(BaseTopNavScreen):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VersionScreen(BaseTopNavScreen):
|
||||||
|
version_name: str = None
|
||||||
|
version_fork: str = None
|
||||||
|
version_timestamp: datetime = None
|
||||||
|
short_commit_hash: str = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
self.title = _("Version")
|
||||||
|
super().__post_init__()
|
||||||
|
|
||||||
|
version_name_font_name = GUIConstants.FIXED_WIDTH_FONT_NAME
|
||||||
|
version_name_font_size = GUIConstants.get_top_nav_title_font_size() + 6
|
||||||
|
version_name_font = Fonts.get_font(version_name_font_name, version_name_font_size)
|
||||||
|
(left, version_name_char_height, version_name_char_width, bottom) = version_name_font.getbbox("X", anchor="ls")
|
||||||
|
|
||||||
|
if len(self.version_name) * version_name_char_width > self.canvas_width - 2*GUIConstants.EDGE_PADDING:
|
||||||
|
max_chars_width = int((self.canvas_width - 2*GUIConstants.EDGE_PADDING) / version_name_char_width)
|
||||||
|
# Add as many line breaks as needed for the version string to fit
|
||||||
|
wrapped_version = []
|
||||||
|
for i in range(0, len(self.version_name), max_chars_width):
|
||||||
|
if i + max_chars_width < len(self.version_name):
|
||||||
|
wrapped_version.append(self.version_name[i:i+max_chars_width])
|
||||||
|
else:
|
||||||
|
wrapped_version.append(self.version_name[i:])
|
||||||
|
self.version_name = "\n".join(wrapped_version)
|
||||||
|
|
||||||
|
timestamp_font_name = GUIConstants.get_body_font_name()
|
||||||
|
timestamp_font_size = GUIConstants.get_body_font_size()
|
||||||
|
timestamp_font = Fonts.get_font(timestamp_font_name, timestamp_font_size)
|
||||||
|
(left, timestamp_char_height, timestamp_char_width, bottom) = timestamp_font.getbbox("UTC", anchor="ls")
|
||||||
|
|
||||||
|
screen_y = self.top_nav.height + GUIConstants.COMPONENT_PADDING * 2
|
||||||
|
if not self.version_fork and not self.short_commit_hash:
|
||||||
|
# Center the version name if there's no fork/commit info
|
||||||
|
screen_y = int(self.canvas_height / 2) - (-1 * version_name_char_height) # char_height is negative
|
||||||
|
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=self.version_name,
|
||||||
|
font_name=version_name_font_name,
|
||||||
|
font_size=version_name_font_size,
|
||||||
|
font_color=GUIConstants.ACCENT_COLOR,
|
||||||
|
screen_y=screen_y,
|
||||||
|
))
|
||||||
|
|
||||||
|
screen_y = self.components[-1].screen_y + self.components[-1].height + 3*GUIConstants.COMPONENT_PADDING
|
||||||
|
|
||||||
|
label_font_name = GUIConstants.get_body_font_name()
|
||||||
|
label_font_size = GUIConstants.get_body_font_size()
|
||||||
|
label_font = Fonts.get_font(label_font_name, label_font_size)
|
||||||
|
(left, label_height, fork_label_width, bottom) = label_font.getbbox("fork: ", anchor="ls")
|
||||||
|
(left, label_height, commit_label_width, bottom) = label_font.getbbox("commit: ", anchor="ls")
|
||||||
|
|
||||||
|
if self.version_fork:
|
||||||
|
screen_x = 0
|
||||||
|
if self.short_commit_hash:
|
||||||
|
# right-align the labels
|
||||||
|
screen_x = commit_label_width - fork_label_width
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=f"fork: ",
|
||||||
|
is_text_centered=False,
|
||||||
|
font_color=GUIConstants.LABEL_FONT_COLOR,
|
||||||
|
screen_x=screen_x,
|
||||||
|
screen_y=screen_y,
|
||||||
|
))
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=self.version_fork,
|
||||||
|
is_text_centered=False,
|
||||||
|
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||||
|
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||||
|
edge_padding=0,
|
||||||
|
screen_x=screen_x + fork_label_width + GUIConstants.COMPONENT_PADDING,
|
||||||
|
screen_y=screen_y,
|
||||||
|
))
|
||||||
|
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
|
||||||
|
|
||||||
|
if self.short_commit_hash:
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=f"commit: ",
|
||||||
|
is_text_centered=False,
|
||||||
|
font_color=GUIConstants.LABEL_FONT_COLOR,
|
||||||
|
screen_y=screen_y,
|
||||||
|
))
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=self.short_commit_hash,
|
||||||
|
is_text_centered=False,
|
||||||
|
font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||||
|
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||||
|
edge_padding=0,
|
||||||
|
screen_x=commit_label_width + GUIConstants.COMPONENT_PADDING,
|
||||||
|
screen_y=screen_y,
|
||||||
|
))
|
||||||
|
|
||||||
|
# Pin the timestamp centered to the bottom of the screen
|
||||||
|
last_edit_str = self.version_timestamp.strftime("%Y-%m-%d %H:%M:%S UTC")
|
||||||
|
self.components.append(TextArea(
|
||||||
|
text=last_edit_str,
|
||||||
|
font_name=timestamp_font_name,
|
||||||
|
font_size=timestamp_font_size,
|
||||||
|
screen_y=self.canvas_height - (-1 * timestamp_char_height) - GUIConstants.EDGE_PADDING,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class SettingsQRConfirmationScreen(ButtonListScreen):
|
class SettingsQRConfirmationScreen(ButtonListScreen):
|
||||||
config_name: str = None
|
config_name: str = None
|
||||||
|
|||||||
@@ -0,0 +1,901 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from seedsigner.models.settings import Settings
|
||||||
|
from seedsigner.models.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Note: If this exception and its associated decorator end up being useful elsewhere, move
|
||||||
|
# them to a more general location (e.g. create a helpers/exceptions.py).
|
||||||
|
class NotAllowedInSeedSignerOS(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def not_allowed_in_seedsigner_os(func: callable):
|
||||||
|
""" Simple decorator to enforce SeedSigner OS restrictions. """
|
||||||
|
def wrapper_func(*args, **kwargs):
|
||||||
|
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||||
|
raise NotAllowedInSeedSignerOS(f"Cannot run `{func.__name__}` in SeedSigner OS.")
|
||||||
|
|
||||||
|
# Now run the target function and return its results
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapper_func
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Version(Singleton):
|
||||||
|
"""
|
||||||
|
Utility class to report the current version and the last edit time of the source code.
|
||||||
|
|
||||||
|
Implemented as a Singleton for convenient access (no need to keep passing an instance
|
||||||
|
around in the main code) and so that the version data is only determined once per
|
||||||
|
runtime.
|
||||||
|
|
||||||
|
SeedSigner OS lifecycle:
|
||||||
|
* The SeedSigner OS build process runs the tools/write_versionfile.py script to
|
||||||
|
generate version.json.
|
||||||
|
* The version.json file is included in the SeedSigner OS image.
|
||||||
|
|
||||||
|
The version data is fetched differently depending on the environment:
|
||||||
|
|
||||||
|
In SeedSigner OS:
|
||||||
|
* Version data is read exclusively from version.json at runtime.
|
||||||
|
|
||||||
|
In the SeedSigner OS builder:
|
||||||
|
- Note: the build process relies on `git` being installed so we leverage it here.
|
||||||
|
* version_name:
|
||||||
|
* read dynamically from `git` shell calls to retrieve, in order:
|
||||||
|
* Current git branch name
|
||||||
|
* Current git tag name
|
||||||
|
* Current git short commit hash
|
||||||
|
* version_fork:
|
||||||
|
* the git repo owner parsed out of the remote url.
|
||||||
|
* https://github.com/SeedSigner/seedsigner.git -> "SeedSigner"
|
||||||
|
* Read dynamically from shell `git` call to check the remote "origin" URL.
|
||||||
|
* short_commit_hash:
|
||||||
|
* the commit hash for the current branch / tag / detached HEAD.
|
||||||
|
* Read dynamically from shell `git` call.
|
||||||
|
* version_timestamp:
|
||||||
|
* the last git commit time for the current branch / tag / detached HEAD.
|
||||||
|
* Read dynamically from shell `git` call.
|
||||||
|
|
||||||
|
In local dev:
|
||||||
|
- Similar process as for the SeedSigner OS builder, but with additional fallbacks and
|
||||||
|
a different method for determining version_timestamp.
|
||||||
|
- If a local version.json is present, it will be ignored; it may be out of date and
|
||||||
|
could lead to confusion.
|
||||||
|
* version_name:
|
||||||
|
* `git` shell calls + directly parsing the .git/HEAD file and .git/refs/tags
|
||||||
|
when necessary.
|
||||||
|
* version_fork:
|
||||||
|
* `git` shell call + parse the .git/config for the remote "origin" URL.
|
||||||
|
* commit_hash:
|
||||||
|
* Shell `git` call + parse the .git/HEAD file and
|
||||||
|
.git/refs/heads/<branch_name> when necessary.
|
||||||
|
* version_timestamp: determined by scanning the src/ directory for the most
|
||||||
|
recently modified python file.
|
||||||
|
|
||||||
|
In Github Actions CI:
|
||||||
|
* version_name: read from GITHUB_REF_NAME env var.
|
||||||
|
* version_fork: read from PR_AUTHOR custom CI env var or GITHUB_REPOSITORY_OWNER.
|
||||||
|
* version_timestamp: Shell `git` call to get the last commit time for the current
|
||||||
|
branch/tag/commit.
|
||||||
|
* commit_hash: read from SOURCE_SHA custom CI env var or GITHUB_SHA.
|
||||||
|
|
||||||
|
This class defines the limited methods that are meant to be publicly accessible
|
||||||
|
across the SeedSigner codebase.
|
||||||
|
|
||||||
|
The utility functions in `VersionUtils` were explicitly isolated because they should
|
||||||
|
NOT be used elsewhere in the codebase.
|
||||||
|
TODO: Should `VersionUtils` be an internal class within `Version` to further signal
|
||||||
|
that it is not to be used externally?
|
||||||
|
TODO: Also pull git log history and display it in its own View?
|
||||||
|
"""
|
||||||
|
_version_name: str = None
|
||||||
|
_version_fork: str = None
|
||||||
|
_short_commit_hash: str = None
|
||||||
|
_version_timestamp: datetime = None
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_instance(cls):
|
||||||
|
""" This is the only way to access the one and only instance. """
|
||||||
|
if not cls._instance:
|
||||||
|
# Instantiate the one and only Version instance
|
||||||
|
version = cls.__new__(cls)
|
||||||
|
cls._instance = version
|
||||||
|
|
||||||
|
# Populate version data
|
||||||
|
version._version_name = VersionUtils.get_version_name()
|
||||||
|
version._version_fork = VersionUtils.get_version_fork()
|
||||||
|
version._version_timestamp = VersionUtils.get_version_timestamp()
|
||||||
|
version._short_commit_hash = VersionUtils.get_short_commit_hash()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_version_name(cls) -> str | None:
|
||||||
|
return cls.get_instance()._version_name
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_version_fork(cls) -> str | None:
|
||||||
|
return cls.get_instance()._version_fork
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_short_commit_hash(cls) -> str | None:
|
||||||
|
return cls.get_instance()._short_commit_hash
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_version_timestamp(cls) -> datetime | None:
|
||||||
|
return cls.get_instance()._version_timestamp
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_release_image(cls) -> bool:
|
||||||
|
"""
|
||||||
|
Returns True if all of the following are true:
|
||||||
|
* We're running in SeedSigner OS.
|
||||||
|
* `version_fork` is the main "seedsigner" repo.
|
||||||
|
* `version_name` corresponds to a "clean" semantic version tag.
|
||||||
|
* e.g. "v0.8.5" but not "v0.8.5-rc1".
|
||||||
|
"""
|
||||||
|
if Settings.HOSTNAME != Settings.SEEDSIGNER_OS:
|
||||||
|
return False
|
||||||
|
|
||||||
|
fork = cls.get_version_fork()
|
||||||
|
if not fork or fork.lower() != "seedsigner":
|
||||||
|
return False
|
||||||
|
|
||||||
|
version = cls.get_version_name()
|
||||||
|
# Even though our release tags in git do not include "v", at this point in the
|
||||||
|
# code the "v" will already be prepended to `version_name` if it is a semantic
|
||||||
|
# version tag. If "v" is prepended to future release tags, logic further up the
|
||||||
|
# chain will already gracefully handle it.
|
||||||
|
if not version or not version.startswith("v"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Is it a clean semantic version?
|
||||||
|
version = version[1:] # strip the "v" prefix
|
||||||
|
for part in version.split("."):
|
||||||
|
if not part.isnumeric():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def override_data(cls, **kwargs):
|
||||||
|
"""
|
||||||
|
Only used by the screenshot generator.
|
||||||
|
"""
|
||||||
|
instance = cls.get_instance()
|
||||||
|
if VersionUtils.VERSIONFILE_ATTR__NAME in kwargs:
|
||||||
|
instance._version_name = kwargs[VersionUtils.VERSIONFILE_ATTR__NAME]
|
||||||
|
if VersionUtils.VERSIONFILE_ATTR__FORK in kwargs:
|
||||||
|
instance._version_fork = kwargs[VersionUtils.VERSIONFILE_ATTR__FORK]
|
||||||
|
if VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH in kwargs:
|
||||||
|
instance._short_commit_hash = kwargs[VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH]
|
||||||
|
if VersionUtils.VERSIONFILE_ATTR__TIMESTAMP in kwargs:
|
||||||
|
instance._version_timestamp = kwargs[VersionUtils.VERSIONFILE_ATTR__TIMESTAMP]
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def to_dict(cls) -> dict:
|
||||||
|
instance = cls.get_instance()
|
||||||
|
return {
|
||||||
|
VersionUtils.VERSIONFILE_ATTR__NAME: instance._version_name,
|
||||||
|
VersionUtils.VERSIONFILE_ATTR__FORK: instance._version_fork,
|
||||||
|
VersionUtils.VERSIONFILE_ATTR__SHORT_COMMIT_HASH: instance._short_commit_hash,
|
||||||
|
VersionUtils.VERSIONFILE_ATTR__TIMESTAMP: instance._version_timestamp.isoformat() if instance._version_timestamp else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class VersionUtils:
|
||||||
|
""" *********************************************************************************
|
||||||
|
Not meant to be used elsewhere in the SeedSigner codebase (aside from
|
||||||
|
tools/write_versionfile.py).
|
||||||
|
|
||||||
|
Methods are separated out here to a rather extreme degree in order to enable all the
|
||||||
|
mocking that is required for testing.
|
||||||
|
|
||||||
|
Summary of functions:
|
||||||
|
* Top-level "get" calls that manage all the possible ways of getting the version
|
||||||
|
data in an environment-aware manner (SeedSigner OS, local dev, Github Actions
|
||||||
|
CI).
|
||||||
|
* Reading data from the version.json file (SeedSigner OS).
|
||||||
|
* Getting data from SeedSigner OS build env vars.
|
||||||
|
* Detecting Github Actions CI environment and getting data from its env vars.
|
||||||
|
* Parsing local filesystem .git/ files.
|
||||||
|
* Making shell `git` calls.
|
||||||
|
* One external http GET to github to get the most recent release tag.
|
||||||
|
********************************************************************************* """
|
||||||
|
|
||||||
|
ENV_VAR__IS_SEEDSIGNER_OS_BUILDER = "SEEDSIGNER_OS_BUILDER"
|
||||||
|
ENV_VAR__GITHUB_ACTIONS__IS_CI = "CI"
|
||||||
|
ENV_VAR__GITHUB_ACTIONS__HEAD_REF = "GITHUB_HEAD_REF"
|
||||||
|
ENV_VAR__GITHUB_ACTIONS__REF_NAME = "GITHUB_REF_NAME"
|
||||||
|
ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA = "SOURCE_SHA"
|
||||||
|
ENV_VAR__GITHUB_ACTIONS__SHA = "GITHUB_SHA"
|
||||||
|
ENV_VAR__GITHUB_ACTIONS__PR_AUTHOR = "PR_AUTHOR"
|
||||||
|
ENV_VAR__GITHUB_ACTIONS__REPOSITORY_OWNER = "GITHUB_REPOSITORY_OWNER"
|
||||||
|
DOT_GIT_DIR_NAME = ".git" # defined to facilitate mocking in tests
|
||||||
|
VERSIONFILE__FILENAME = "version.json"
|
||||||
|
VERSIONFILE_ATTR__NAME = "name"
|
||||||
|
VERSIONFILE_ATTR__FORK = "fork"
|
||||||
|
VERSIONFILE_ATTR__SHORT_COMMIT_HASH = "short_commit_hash"
|
||||||
|
VERSIONFILE_ATTR__TIMESTAMP = "timestamp"
|
||||||
|
|
||||||
|
|
||||||
|
""" *************************************************************************************
|
||||||
|
Top-level "get" calls. These are the only functions meant to be called externally.
|
||||||
|
************************************************************************************* """
|
||||||
|
@classmethod
|
||||||
|
def get_version_name(cls) -> str:
|
||||||
|
"""
|
||||||
|
Will prefix the version name "v" if it looks like a semantic version.
|
||||||
|
"""
|
||||||
|
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||||
|
# The SeedSigner OS build process generates the version.json file for the tag,
|
||||||
|
# branch, or commit hash the image is targeting.
|
||||||
|
version_name = VersionUtils._get_version_name_from_version_file()
|
||||||
|
if version_name is None:
|
||||||
|
# Shouldn't be possible. Raise an exception to alert testers before this
|
||||||
|
# image goes out.
|
||||||
|
raise Exception("Could not read the version from the version.json file.")
|
||||||
|
|
||||||
|
# Note: the version.json file already contains any necessary pre-processing so
|
||||||
|
# if we're on a semantic version tag, version_name will already be prefixed
|
||||||
|
# with "v".
|
||||||
|
return version_name
|
||||||
|
|
||||||
|
elif VersionUtils.is_github_actions_ci():
|
||||||
|
# In Github Actions CI, try to get the version name from env vars
|
||||||
|
version_name = VersionUtils._get_version_name_from_github_actions_env_vars()
|
||||||
|
if version_name is not None:
|
||||||
|
return VersionUtils._prefix_version_name(version_name)
|
||||||
|
else:
|
||||||
|
raise Exception("Could not determine version from Github Actions env vars.")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# In the SeedSigner OS builder we know that we'll have the `git` shell
|
||||||
|
# commands available. We add extra fallbacks for local dev.
|
||||||
|
for get_version_name_method in [
|
||||||
|
VersionUtils._get_version_name_from_git_shell,
|
||||||
|
VersionUtils._get_version_name_from_git_HEAD,
|
||||||
|
]:
|
||||||
|
version_name = get_version_name_method()
|
||||||
|
if version_name is not None:
|
||||||
|
return VersionUtils._prefix_version_name(version_name)
|
||||||
|
|
||||||
|
# If we reach here, none of the methods worked
|
||||||
|
# TODO: What do we want to do in this case?
|
||||||
|
# Intentionally not marking this for translation; end users should never see it.
|
||||||
|
return "version not detected"
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_version_fork(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Returns the fork owner or None if it cannot be determined.
|
||||||
|
|
||||||
|
e.g. https://github.com/SeedSigner/seedsigner -> "SeedSigner"
|
||||||
|
"""
|
||||||
|
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||||
|
# The SeedSigner OS build process generates the version.json file which will
|
||||||
|
# already contain the fork name.
|
||||||
|
return VersionUtils._get_version_fork_from_version_file()
|
||||||
|
|
||||||
|
elif VersionUtils.is_github_actions_ci():
|
||||||
|
# In Github Actions CI, try to get the fork name from env vars.
|
||||||
|
return VersionUtils._get_version_fork_from_github_actions_env_vars()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# In the SeedSigner OS builder we know that we'll have the `git` shell
|
||||||
|
# commands available. We add extra fallbacks for local dev.
|
||||||
|
for get_version_fork_method in [
|
||||||
|
VersionUtils._get_version_fork_from_git_shell,
|
||||||
|
VersionUtils._get_version_fork_from_git_config,
|
||||||
|
]:
|
||||||
|
version_fork = get_version_fork_method()
|
||||||
|
if version_fork is not None:
|
||||||
|
return version_fork
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_short_commit_hash(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Returns the short commit hash string.
|
||||||
|
|
||||||
|
Will be None if the local dev system has no git state available or if it only has
|
||||||
|
the .git/HEAD but is currently on a branch (not a tag or specific commit).
|
||||||
|
"""
|
||||||
|
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||||
|
return VersionUtils._get_short_commit_hash_from_version_file()
|
||||||
|
|
||||||
|
full_commit_hash = None
|
||||||
|
if VersionUtils.is_github_actions_ci():
|
||||||
|
# In Github Actions CI the "SHA" env var should always be available
|
||||||
|
full_commit_hash = VersionUtils._get_full_commit_hash_from_github_actions_env_vars()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# In the SeedSigner OS builder we know that we'll have the `git` shell
|
||||||
|
# commands available. We add extra fallbacks for local dev.
|
||||||
|
for get_full_commit_hash_method in [
|
||||||
|
VersionUtils._get_full_commit_hash_from_git_shell,
|
||||||
|
VersionUtils._get_full_commit_hash_from_git_HEAD,
|
||||||
|
]:
|
||||||
|
full_commit_hash = get_full_commit_hash_method()
|
||||||
|
if full_commit_hash is not None:
|
||||||
|
break
|
||||||
|
|
||||||
|
if full_commit_hash is None:
|
||||||
|
# `git` shell calls didn't work and HEAD might be on a branch, so dig
|
||||||
|
# deeper into the .git files to look up the commit hash via the branch
|
||||||
|
# name.
|
||||||
|
branch_name, expect_full_commit_hash_to_be_none = VersionUtils._read_git_HEAD_file()
|
||||||
|
if branch_name:
|
||||||
|
full_commit_hash = VersionUtils._get_full_commit_hash_from_git_refs_heads(branch_name)
|
||||||
|
|
||||||
|
if full_commit_hash is not None:
|
||||||
|
return full_commit_hash[:7]
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_version_timestamp(cls) -> datetime:
|
||||||
|
"""
|
||||||
|
Returns a datetime object representing the last edit time of the source code via
|
||||||
|
the most recent git commit time (SeedSigner OS, as written in version.json) or
|
||||||
|
the most recently modified python source file (local dev).
|
||||||
|
"""
|
||||||
|
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||||
|
# The SeedSigner OS build process generates the version.json file which will
|
||||||
|
# already contain the last edit time.
|
||||||
|
version_timestamp = VersionUtils._get_version_timestamp_from_version_file()
|
||||||
|
if version_timestamp is None:
|
||||||
|
# Shouldn't be possible. Raise an exception to alert testers before this
|
||||||
|
# image goes out.
|
||||||
|
raise Exception("Could not read the version timestamp from the version.json file.")
|
||||||
|
return datetime.fromisoformat(version_timestamp)
|
||||||
|
|
||||||
|
elif VersionUtils.is_github_actions_ci():
|
||||||
|
# In Github Actions CI `git` shell call should be available
|
||||||
|
return VersionUtils._get_version_timestamp_from_git_shell()
|
||||||
|
|
||||||
|
elif VersionUtils._is_seedsigner_os_builder_env():
|
||||||
|
# In the SeedSigner OS builder we know that we'll have the `git` shell
|
||||||
|
# commands available.
|
||||||
|
# Note: This is the only piece of version data that behaves differently
|
||||||
|
# between the SeedSigner OS builder and local dev.
|
||||||
|
return VersionUtils._get_version_timestamp_from_git_shell()
|
||||||
|
|
||||||
|
else:
|
||||||
|
# In local dev we change our approach and instead use the last modified time
|
||||||
|
# of the source python files.
|
||||||
|
return VersionUtils._get_last_modified_timestamp_from_src_files()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
""" *************************************************************************************
|
||||||
|
Misc internal utility functions.
|
||||||
|
************************************************************************************* """
|
||||||
|
@classmethod
|
||||||
|
def _prefix_version_name(cls, version_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Ensure version strings are prefixed with 'v' if they look like semantic
|
||||||
|
versions. Only checks that the first part is numeric in order to be compatible
|
||||||
|
with non-numeric minor versions (e.g. "0.8.5-rc1").
|
||||||
|
"""
|
||||||
|
if not version_name.startswith("v") and version_name.count(".") >= 1 and version_name.split(".")[0].isnumeric():
|
||||||
|
return f"v{version_name}"
|
||||||
|
return version_name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
""" *************************************************************************************
|
||||||
|
Reading data from the version.json file.
|
||||||
|
************************************************************************************* """
|
||||||
|
@classmethod
|
||||||
|
def _get_version_file_path(cls) -> str:
|
||||||
|
# Have to back out of this file's location in the "helpers" dir to the main
|
||||||
|
# "seedsigner" dir.
|
||||||
|
return os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", cls.VERSIONFILE__FILENAME))
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _read_version_file(cls) -> dict | None:
|
||||||
|
"""
|
||||||
|
Attempts to read version.json and return its contents as a dict.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with open(cls._get_version_file_path(), "r") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.debug(f"Ignoring {e}")
|
||||||
|
except Exception as e:
|
||||||
|
# Something unexpected happened. Flag it but keep going.
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_version_name_from_version_file(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Attempts to read the version.json and return the version name.
|
||||||
|
"""
|
||||||
|
version_data = cls._read_version_file()
|
||||||
|
if version_data:
|
||||||
|
return version_data.get(cls.VERSIONFILE_ATTR__NAME)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_version_fork_from_version_file(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Attempts to read the version.json and return the version fork name.
|
||||||
|
"""
|
||||||
|
version_data = cls._read_version_file()
|
||||||
|
if version_data:
|
||||||
|
return version_data.get(cls.VERSIONFILE_ATTR__FORK)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_short_commit_hash_from_version_file(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Attempts to read the version.json and return the version commit hash.
|
||||||
|
"""
|
||||||
|
version_data = cls._read_version_file()
|
||||||
|
if version_data:
|
||||||
|
return version_data.get(cls.VERSIONFILE_ATTR__SHORT_COMMIT_HASH)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_version_timestamp_from_version_file(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Attempts to read the version.json and return the version timestamp.
|
||||||
|
"""
|
||||||
|
version_data = cls._read_version_file()
|
||||||
|
if version_data:
|
||||||
|
return version_data.get(cls.VERSIONFILE_ATTR__TIMESTAMP)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
""" *************************************************************************************
|
||||||
|
Utilities used in the SeedSigner OS build environment and writing the version.json file.
|
||||||
|
************************************************************************************* """
|
||||||
|
@classmethod
|
||||||
|
def _is_seedsigner_os_builder_env(cls) -> bool:
|
||||||
|
"""
|
||||||
|
The `ENV_VAR__IS_SEEDSIGNER_OS_BUILDER` env var is set during the SeedSigner OS
|
||||||
|
build process.
|
||||||
|
"""
|
||||||
|
return os.getenv(cls.ENV_VAR__IS_SEEDSIGNER_OS_BUILDER) is not None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
""" *************************************************************************************
|
||||||
|
Getting data from Github Actions CI env vars.
|
||||||
|
************************************************************************************* """
|
||||||
|
@classmethod
|
||||||
|
def is_github_actions_ci(cls) -> bool:
|
||||||
|
return os.getenv(cls.ENV_VAR__GITHUB_ACTIONS__IS_CI) == "true"
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_version_name_from_github_actions_env_vars(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
HEAD_REF: head ref or source branch. But only present for PRs.
|
||||||
|
REF_NAME: branch or tag name. But it is "<pr_number>/merge" for unmerged PRs (not
|
||||||
|
what we want)
|
||||||
|
SOURCE_SHA is the full commit hash of the commit that triggered the workflow (if
|
||||||
|
it's a PR).
|
||||||
|
SHA is the full commit hash; not expecting to ever need this fallback.
|
||||||
|
"""
|
||||||
|
for env_var in [
|
||||||
|
cls.ENV_VAR__GITHUB_ACTIONS__HEAD_REF,
|
||||||
|
cls.ENV_VAR__GITHUB_ACTIONS__REF_NAME,
|
||||||
|
cls.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA,
|
||||||
|
cls.ENV_VAR__GITHUB_ACTIONS__SHA
|
||||||
|
]:
|
||||||
|
version_name = os.getenv(env_var)
|
||||||
|
if version_name:
|
||||||
|
if env_var in [cls.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA, cls.ENV_VAR__GITHUB_ACTIONS__SHA]:
|
||||||
|
# Return the short version
|
||||||
|
version_name = version_name[:7]
|
||||||
|
return version_name
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_version_fork_from_github_actions_env_vars(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
PR_AUTHOR: Set by the .github/workflows/tests.yml workflow. Should be the PR author.
|
||||||
|
REPOSITORY_OWNER: the repo owner, usually the main "SeedSigner" org.
|
||||||
|
"""
|
||||||
|
for env_var in [
|
||||||
|
cls.ENV_VAR__GITHUB_ACTIONS__PR_AUTHOR,
|
||||||
|
cls.ENV_VAR__GITHUB_ACTIONS__REPOSITORY_OWNER
|
||||||
|
]:
|
||||||
|
fork_name = os.getenv(env_var)
|
||||||
|
if fork_name:
|
||||||
|
return fork_name
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_full_commit_hash_from_github_actions_env_vars(cls) -> str | None:
|
||||||
|
for env_var in [
|
||||||
|
cls.ENV_VAR__GITHUB_ACTIONS__SOURCE_SHA,
|
||||||
|
cls.ENV_VAR__GITHUB_ACTIONS__SHA
|
||||||
|
]:
|
||||||
|
commit_hash = os.getenv(env_var)
|
||||||
|
if commit_hash:
|
||||||
|
return commit_hash
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
""" *************************************************************************************
|
||||||
|
Get data via shell `git` commands.
|
||||||
|
|
||||||
|
These calls shouldn't be dangerous, but we definitely don't want them being run in
|
||||||
|
SeedSigner OS regardless so we apply the decorator restriction.
|
||||||
|
************************************************************************************* """
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_branch_name_from_git_shell(cls) -> str | None:
|
||||||
|
branch_name = os.popen("git branch --show-current 2> /dev/null").read()
|
||||||
|
return branch_name.strip() if branch_name else None
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_tag_name_from_git_shell(cls) -> str | None:
|
||||||
|
# Only return a value if the current commit exactly corresponds with a tag.
|
||||||
|
# (`--points-at` defaults to the current HEAD)
|
||||||
|
tag_name = os.popen(f"git tag --points-at 2> /dev/null").read()
|
||||||
|
return tag_name.strip() if tag_name else None
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_full_commit_hash_from_git_shell(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Attempts to get the current full commit hash via shell `git` commands.
|
||||||
|
"""
|
||||||
|
commit_hash = os.popen("git rev-parse HEAD").read()
|
||||||
|
return commit_hash.strip() if commit_hash else None
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_short_commit_hash_from_git_shell(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Attempts to get the current short commit hash via shell `git` commands.
|
||||||
|
"""
|
||||||
|
commit_hash = cls._get_full_commit_hash_from_git_shell()
|
||||||
|
return commit_hash[:7] if commit_hash else None
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_version_name_from_git_shell(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Attempts to get the version name via shell `git` commands.
|
||||||
|
|
||||||
|
Note: If we have to fall back to the commit hash, we use the short version.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
cls._get_branch_name_from_git_shell() or
|
||||||
|
cls._get_tag_name_from_git_shell() or
|
||||||
|
cls._get_short_commit_hash_from_git_shell()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_version_fork_from_git_shell(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Attempts to get the fork owner name via shell `git` commands.
|
||||||
|
"""
|
||||||
|
# We expect to at least have a remote named "origin"
|
||||||
|
remote_url = os.popen("git remote get-url origin 2> /dev/null").read().strip()
|
||||||
|
return cls._parse_git_remote_url(remote_url) if remote_url else None
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_version_timestamp_from_git_shell(cls) -> datetime | None:
|
||||||
|
version_timestamp = os.popen("git log -1 --format=%cI").read().strip()
|
||||||
|
if version_timestamp:
|
||||||
|
if version_timestamp.endswith("Z"):
|
||||||
|
# Git outputs UTC time with a "Z" suffix; datetime.fromisoformat()
|
||||||
|
# doesn't like the "Z" so we replace it with "+00:00"
|
||||||
|
# TODO: Python 3.11+ has fromisoformat() support for "Z" suffixes so this
|
||||||
|
# can be removed once we drop python 3.10 support.
|
||||||
|
version_timestamp = version_timestamp.replace("Z", "+00:00")
|
||||||
|
|
||||||
|
# Parse the timestamp, ensure that it's in UTC, and omit tz info
|
||||||
|
return datetime.fromisoformat(version_timestamp).astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
""" *************************************************************************************
|
||||||
|
Reading directly from local .git/ or src/ files.
|
||||||
|
|
||||||
|
These operations aren't dangerous but aren't necessary when we're running in
|
||||||
|
SeedSigner OS, so we apply the decorator restriction here.
|
||||||
|
************************************************************************************* """
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_dot_git_dir(cls) -> str:
|
||||||
|
# If it exists, the .git dir will be in the project root
|
||||||
|
path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# Have to back out of this file's location in "helpers" and "seedsigner" and "src" dirs
|
||||||
|
project_root = os.path.join(path, "..", "..", "..")
|
||||||
|
|
||||||
|
return os.path.normpath(os.path.join(project_root, cls.DOT_GIT_DIR_NAME))
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _read_git_HEAD_file(cls) -> tuple[str | None, str | None]:
|
||||||
|
"""
|
||||||
|
Reads the .git/HEAD file and returns a tuple of (branch_name, full_commit_hash) where
|
||||||
|
only one will have a value.
|
||||||
|
|
||||||
|
If there is no .git/HEAD file detected, both values will be None.
|
||||||
|
"""
|
||||||
|
git_HEAD_path = os.path.join(cls._get_dot_git_dir(), "HEAD")
|
||||||
|
|
||||||
|
branch_name = None
|
||||||
|
full_commit_hash = None
|
||||||
|
try:
|
||||||
|
with open(git_HEAD_path, "r") as f:
|
||||||
|
git_ref = f.read().strip()
|
||||||
|
if git_ref.startswith("ref:"):
|
||||||
|
# HEAD format: "ref: refs/heads/some_branch_name"
|
||||||
|
branch_name = git_ref.split("/")[-1]
|
||||||
|
else:
|
||||||
|
# If we're on a detached HEAD, the contents will just be the current
|
||||||
|
# full commit hash.
|
||||||
|
full_commit_hash = git_ref
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.debug(f"Ignoring {e}")
|
||||||
|
except Exception as e:
|
||||||
|
# Something unexpected happened. Flag it but keep going.
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return (branch_name, full_commit_hash)
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_matching_tag_from_git_refs_tags(cls, commit_hash: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Checks the .git/refs/tags dir for a tag that matches the provided commit hash.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
git_refs_tags_dir = os.path.join(cls._get_dot_git_dir(), "refs", "tags")
|
||||||
|
for tag_filename in os.listdir(git_refs_tags_dir):
|
||||||
|
tag_path = os.path.join(git_refs_tags_dir, tag_filename)
|
||||||
|
with open(tag_path, "r") as tag_file:
|
||||||
|
# Tag files just contain their associated commit hash
|
||||||
|
tag_commit_hash = tag_file.read().strip()
|
||||||
|
if tag_commit_hash == commit_hash:
|
||||||
|
# Filename is the tag name
|
||||||
|
return tag_filename
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.debug(f"Ignoring {e}")
|
||||||
|
except Exception as e:
|
||||||
|
# Something unexpected happened. Flag it but keep going.
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_version_name_from_git_HEAD(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Reads the .git/HEAD file and, depending on the local dev git state, returns
|
||||||
|
either the branch name, tag name, or short commit hash.
|
||||||
|
"""
|
||||||
|
branch_name, commit_hash = VersionUtils._read_git_HEAD_file()
|
||||||
|
if branch_name:
|
||||||
|
return branch_name
|
||||||
|
elif commit_hash:
|
||||||
|
# See if this commit_hash matches a tag
|
||||||
|
matching_tag = VersionUtils._get_matching_tag_from_git_refs_tags(commit_hash)
|
||||||
|
if matching_tag:
|
||||||
|
return matching_tag
|
||||||
|
else:
|
||||||
|
return commit_hash[:7] # short commit hash
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_full_commit_hash_from_git_HEAD(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Reads the .git/HEAD file and, depending on the local dev git state, returns
|
||||||
|
the full commit hash. Will only return a value if HEAD is detached (on a tag or
|
||||||
|
on a specific commit).
|
||||||
|
"""
|
||||||
|
branch_name, commit_hash = VersionUtils._read_git_HEAD_file()
|
||||||
|
return commit_hash
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_full_commit_hash_from_git_refs_heads(cls, branch_name: str) -> str | None:
|
||||||
|
"""
|
||||||
|
When HEAD is on a branch, it doesn't tell us the commit hash directly. So we have
|
||||||
|
to look it up here in .git/refs/heads/<branch_name>.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
git_ref_path = os.path.join(cls._get_dot_git_dir(), "refs", "heads", branch_name)
|
||||||
|
with open(git_ref_path, "r") as f:
|
||||||
|
commit_hash = f.read().strip()
|
||||||
|
return commit_hash
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.debug(f"Ignoring {e}")
|
||||||
|
except Exception as e:
|
||||||
|
# Something unexpected happened. Flag it but keep going.
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_git_remote_url(cls, remote_url: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Parses a git remote URL to extract the fork owner name.
|
||||||
|
|
||||||
|
Formats:
|
||||||
|
* https://github.com/repo_owner/repo_name.git
|
||||||
|
* git@github.com:repo_owner/repo_name.git
|
||||||
|
"""
|
||||||
|
if not remote_url:
|
||||||
|
return None
|
||||||
|
if remote_url.startswith("https"):
|
||||||
|
parts = remote_url.rsplit("/", 2)
|
||||||
|
return parts[-2]
|
||||||
|
elif remote_url.startswith("git@"):
|
||||||
|
parts = remote_url.split(":")
|
||||||
|
owner_repo = parts[1] # repo_owner/repo_name.git
|
||||||
|
return owner_repo.split("/")[0]
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_version_fork_from_git_config(cls) -> str | None:
|
||||||
|
"""
|
||||||
|
Attempts to read the .git/config file to determine the remote "origin" URL
|
||||||
|
and extract the fork owner name from it.
|
||||||
|
|
||||||
|
Format:
|
||||||
|
[some_section]
|
||||||
|
some_key = some_value
|
||||||
|
[remote "origin"]
|
||||||
|
url = git@github.com:SeedSigner/seedsigner.git
|
||||||
|
fetch = +refs/heads/*:refs/remotes/origin/*
|
||||||
|
[next_section]
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
git_config_path = os.path.join(cls._get_dot_git_dir(), "config")
|
||||||
|
with open(git_config_path, "r") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
in_origin_section = False
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("[remote \"origin\"]"):
|
||||||
|
# Next lines fetched will be the ones we care about
|
||||||
|
in_origin_section = True
|
||||||
|
elif in_origin_section and line.startswith("url ="):
|
||||||
|
# Found our "origin" url
|
||||||
|
remote_url = line.split("=", 1)[1].strip()
|
||||||
|
return cls._parse_git_remote_url(remote_url)
|
||||||
|
elif in_origin_section and line.startswith("["):
|
||||||
|
# We reached the next section without finding the url
|
||||||
|
raise Exception("Didn't find 'url' entry in 'origin' section of .git/config")
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
logger.debug(f"Ignoring {e}")
|
||||||
|
except Exception as e:
|
||||||
|
# Something unexpected happened. Flag it but keep going.
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _get_last_modified_timestamp_from_src_files(cls) -> datetime | None:
|
||||||
|
"""
|
||||||
|
Recursively scan the src/ directory for the most recent python file edit time.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
path = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# Have to back out of "helpers" and "seedsigner" dirs
|
||||||
|
src_path = os.path.join(path, "..", "..")
|
||||||
|
|
||||||
|
last_modified = 0.0
|
||||||
|
num_files = 0
|
||||||
|
for dirpath, dirnames, filenames in os.walk(src_path):
|
||||||
|
if "__pycache__" in dirpath:
|
||||||
|
continue
|
||||||
|
for filename in filenames:
|
||||||
|
if filename.endswith(".py"):
|
||||||
|
num_files += 1
|
||||||
|
filepath = os.path.join(dirpath, filename)
|
||||||
|
|
||||||
|
# getmtime returns the file's last modified time
|
||||||
|
file_mtime = os.path.getmtime(filepath)
|
||||||
|
last_modified = max(file_mtime, last_modified)
|
||||||
|
if num_files == 0:
|
||||||
|
# Shouldn't be possible
|
||||||
|
raise Exception("No python source files found in src/ directory")
|
||||||
|
|
||||||
|
return datetime.fromtimestamp(last_modified).astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Catch and log any unexpected errors but this isn't a mission-critical
|
||||||
|
# function so return gracefully.
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
""" *************************************************************************************
|
||||||
|
External http GET call to github.
|
||||||
|
************************************************************************************* """
|
||||||
|
@classmethod
|
||||||
|
@not_allowed_in_seedsigner_os
|
||||||
|
def _fetch_latest_seedsigner_release_tag(cls) -> tuple[str, datetime] | tuple[None, None]:
|
||||||
|
"""
|
||||||
|
Fetches the latest release version from the SeedSigner github repo via the
|
||||||
|
github API. Then attempts to resolve the tag name locally to get its associated
|
||||||
|
commit timestamp. If local git data is not available, falls back to using the
|
||||||
|
release published_at time from the API.
|
||||||
|
|
||||||
|
This is only used by the screenshot renderer.
|
||||||
|
"""
|
||||||
|
import urllib.request
|
||||||
|
from http.client import HTTPResponse
|
||||||
|
|
||||||
|
try:
|
||||||
|
"""
|
||||||
|
Excerpted example response from the github API:
|
||||||
|
{
|
||||||
|
"url": "https://api.github.com/repos/SeedSigner/seedsigner/releases/228915183",
|
||||||
|
"tag_name": "0.8.6",
|
||||||
|
"prerelease": false,
|
||||||
|
"created_at": "2025-06-22T01:38:41Z",
|
||||||
|
"updated_at": "2025-06-30T20:49:22Z",
|
||||||
|
"published_at": "2025-06-30T20:45:05Z"
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
req = urllib.request.Request(
|
||||||
|
"https://api.github.com/repos/SeedSigner/seedsigner/releases/latest"
|
||||||
|
)
|
||||||
|
response: HTTPResponse = urllib.request.urlopen(req, timeout=5)
|
||||||
|
if response.status == 200:
|
||||||
|
release_data = json.loads(response.read().decode('utf-8'))
|
||||||
|
version_name = release_data.get("tag_name")
|
||||||
|
|
||||||
|
# Try to find the commit timestamp for this tag from local git data
|
||||||
|
version_timestamp = os.popen(f"git show {version_name} --format=%cI").read().strip()
|
||||||
|
if not version_timestamp:
|
||||||
|
# Fallback: use the release published_at time from the API
|
||||||
|
version_timestamp = release_data.get("published_at").replace("Z", "+00:00")
|
||||||
|
version_timestamp = datetime.fromisoformat(version_timestamp).astimezone(timezone.utc).replace(tzinfo=None)
|
||||||
|
return (VersionUtils._prefix_version_name(version_name), version_timestamp)
|
||||||
|
else:
|
||||||
|
logger.error(f"GitHub API returned status code {response.status}: {response}")
|
||||||
|
return (None, None)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error fetching latest release version:\n{traceback.format_exc()}")
|
||||||
|
return (None, None)
|
||||||
@@ -8,6 +8,7 @@ from gettext import gettext as _
|
|||||||
|
|
||||||
from seedsigner.gui.components import Fonts, GUIConstants, load_image
|
from seedsigner.gui.components import Fonts, GUIConstants, load_image
|
||||||
from seedsigner.gui.screens.screen import BaseScreen
|
from seedsigner.gui.screens.screen import BaseScreen
|
||||||
|
from seedsigner.helpers.version import Version
|
||||||
from seedsigner.models.settings import Settings
|
from seedsigner.models.settings import Settings
|
||||||
from seedsigner.models.settings_definition import SettingsConstants
|
from seedsigner.models.settings_definition import SettingsConstants
|
||||||
from seedsigner.views.view import View
|
from seedsigner.views.view import View
|
||||||
@@ -97,13 +98,17 @@ class OpeningSplashScreen(LogoScreen):
|
|||||||
|
|
||||||
# Display version num below SeedSigner logo
|
# Display version num below SeedSigner logo
|
||||||
font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_top_nav_title_font_size())
|
font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_top_nav_title_font_size())
|
||||||
version = f"v{controller.VERSION}"
|
version = Version.get_version_name()
|
||||||
|
|
||||||
# The logo png is 240x240, but the actual logo is 70px tall, vertically centered
|
# The logo png is 240x240, but the actual logo is 70px tall, vertically centered
|
||||||
logo_height = 70
|
logo_height = 70
|
||||||
version_x = int(self.renderer.canvas_width/2)
|
version_x = int(self.renderer.canvas_width/2)
|
||||||
version_y = int(self.canvas_height/2) + int(logo_height/2) + logo_offset_y + GUIConstants.COMPONENT_PADDING
|
version_y = int(self.canvas_height/2) + int(logo_height/2) + logo_offset_y + GUIConstants.COMPONENT_PADDING
|
||||||
self.renderer.draw.text(xy=(version_x, version_y), text=version, font=font, fill=GUIConstants.ACCENT_COLOR, anchor="mt")
|
version_max_chars = 20
|
||||||
|
self.renderer.draw.text(xy=(version_x, version_y), text=version[:version_max_chars], font=font, fill=GUIConstants.ACCENT_COLOR, anchor="mt")
|
||||||
|
if len(version) > version_max_chars:
|
||||||
|
# Squeeze a second version display line in if needed
|
||||||
|
self.renderer.draw.text(xy=(version_x, version_y + GUIConstants.get_top_nav_title_font_size()), text=version[version_max_chars:], font=font, fill=GUIConstants.ACCENT_COLOR, anchor="mt")
|
||||||
|
|
||||||
if not self.renderer.is_screenshot_generator:
|
if not self.renderer.is_screenshot_generator:
|
||||||
self.renderer.show_image()
|
self.renderer.show_image()
|
||||||
|
|||||||
@@ -17,11 +17,13 @@ class SettingsMenuView(View):
|
|||||||
HARDWARE = ButtonOption("Hardware", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT)
|
HARDWARE = ButtonOption("Hardware", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT)
|
||||||
IO_TEST = ButtonOption("I/O test")
|
IO_TEST = ButtonOption("I/O test")
|
||||||
DONATE = ButtonOption("Donate")
|
DONATE = ButtonOption("Donate")
|
||||||
|
VERSION = ButtonOption("Version")
|
||||||
|
|
||||||
def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0):
|
def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, selected_button_option: ButtonOption = None, initial_scroll: int = 0):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.visibility = visibility
|
self.visibility = visibility
|
||||||
self.selected_attr = selected_attr
|
self.selected_attr = selected_attr
|
||||||
|
self.selected_button_option = selected_button_option
|
||||||
|
|
||||||
# Used to preserve the rendering position in the list
|
# Used to preserve the rendering position in the list
|
||||||
self.initial_scroll = initial_scroll
|
self.initial_scroll = initial_scroll
|
||||||
@@ -33,13 +35,6 @@ class SettingsMenuView(View):
|
|||||||
)
|
)
|
||||||
button_data: list[ButtonOption] = [ButtonOption(e.display_name) for e in settings_entries]
|
button_data: list[ButtonOption] = [ButtonOption(e.display_name) for e in settings_entries]
|
||||||
|
|
||||||
selected_button = 0
|
|
||||||
if self.selected_attr:
|
|
||||||
for i, entry in enumerate(settings_entries):
|
|
||||||
if entry.attr_name == self.selected_attr:
|
|
||||||
selected_button = i
|
|
||||||
break
|
|
||||||
|
|
||||||
if self.visibility == SettingsConstants.VISIBILITY__GENERAL:
|
if self.visibility == SettingsConstants.VISIBILITY__GENERAL:
|
||||||
title = _("Settings")
|
title = _("Settings")
|
||||||
|
|
||||||
@@ -49,6 +44,7 @@ class SettingsMenuView(View):
|
|||||||
|
|
||||||
button_data.append(self.IO_TEST)
|
button_data.append(self.IO_TEST)
|
||||||
button_data.append(self.DONATE)
|
button_data.append(self.DONATE)
|
||||||
|
button_data.append(self.VERSION)
|
||||||
|
|
||||||
elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED:
|
elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED:
|
||||||
title = _("Advanced")
|
title = _("Advanced")
|
||||||
@@ -65,6 +61,15 @@ class SettingsMenuView(View):
|
|||||||
title = _("Dev Options")
|
title = _("Dev Options")
|
||||||
next_destination = None
|
next_destination = None
|
||||||
|
|
||||||
|
selected_button = 0
|
||||||
|
if self.selected_button_option:
|
||||||
|
selected_button = button_data.index(self.selected_button_option)
|
||||||
|
elif self.selected_attr:
|
||||||
|
for i, entry in enumerate(settings_entries):
|
||||||
|
if entry.attr_name == self.selected_attr:
|
||||||
|
selected_button = i
|
||||||
|
break
|
||||||
|
|
||||||
selected_menu_num = self.run_screen(
|
selected_menu_num = self.run_screen(
|
||||||
ButtonListScreen,
|
ButtonListScreen,
|
||||||
title=title,
|
title=title,
|
||||||
@@ -97,6 +102,9 @@ class SettingsMenuView(View):
|
|||||||
elif button_data[selected_menu_num] == self.DONATE:
|
elif button_data[selected_menu_num] == self.DONATE:
|
||||||
return Destination(DonateView)
|
return Destination(DonateView)
|
||||||
|
|
||||||
|
elif button_data[selected_menu_num] == self.VERSION:
|
||||||
|
return Destination(VersionView)
|
||||||
|
|
||||||
elif settings_entries[selected_menu_num].attr_name == SettingsConstants.SETTING__LOCALE:
|
elif settings_entries[selected_menu_num].attr_name == SettingsConstants.SETTING__LOCALE:
|
||||||
return Destination(LocaleSelectionView)
|
return Destination(LocaleSelectionView)
|
||||||
|
|
||||||
@@ -353,3 +361,27 @@ class DonateView(View):
|
|||||||
self.run_screen(settings_screens.DonateScreen)
|
self.run_screen(settings_screens.DonateScreen)
|
||||||
|
|
||||||
return Destination(SettingsMenuView)
|
return Destination(SettingsMenuView)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class VersionView(View):
|
||||||
|
def run(self):
|
||||||
|
from seedsigner.helpers.version import Version
|
||||||
|
|
||||||
|
version_fork = Version.get_version_fork()
|
||||||
|
short_commit_hash = Version.get_short_commit_hash()
|
||||||
|
|
||||||
|
if Version.is_release_image():
|
||||||
|
# Don't display fork name or commit hash for release images
|
||||||
|
version_fork = None
|
||||||
|
short_commit_hash = None
|
||||||
|
|
||||||
|
self.run_screen(
|
||||||
|
settings_screens.VersionScreen,
|
||||||
|
version_name=Version.get_version_name(),
|
||||||
|
version_fork=version_fork,
|
||||||
|
version_timestamp=Version.get_version_timestamp(),
|
||||||
|
short_commit_hash=short_commit_hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
return Destination(SettingsMenuView)
|
||||||
@@ -14,6 +14,8 @@ from embit import compact
|
|||||||
from embit.psbt import PSBT, OutputScope
|
from embit.psbt import PSBT, OutputScope
|
||||||
from embit.script import Script
|
from embit.script import Script
|
||||||
|
|
||||||
|
from seedsigner.helpers.version import Version, VersionUtils
|
||||||
|
|
||||||
# Prevent importing modules w/Raspi hardware dependencies.
|
# Prevent importing modules w/Raspi hardware dependencies.
|
||||||
# These must precede any SeedSigner imports.
|
# These must precede any SeedSigner imports.
|
||||||
sys.modules['seedsigner.hardware.displays.st7789_mpy'] = MagicMock()
|
sys.modules['seedsigner.hardware.displays.st7789_mpy'] = MagicMock()
|
||||||
@@ -114,6 +116,11 @@ seed_24_w_passphrase = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9"
|
|||||||
|
|
||||||
MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx"""
|
MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx"""
|
||||||
|
|
||||||
|
# Grab the most recent release version info
|
||||||
|
(latest_release_version_name, latest_release_version_timestamp) = VersionUtils._fetch_latest_seedsigner_release_tag()
|
||||||
|
if not latest_release_version_name or not latest_release_version_timestamp:
|
||||||
|
print("Could not fetch latest release version from GitHub")
|
||||||
|
|
||||||
|
|
||||||
# Wrap QRDisplayScreen's `render_brightness_tip` in a simple View + Screen so we
|
# Wrap QRDisplayScreen's `render_brightness_tip` in a simple View + Screen so we
|
||||||
# can call it outside of its child thread and generate a screenshot.
|
# can call it outside of its child thread and generate a screenshot.
|
||||||
@@ -211,6 +218,9 @@ def generate_screenshots(locale):
|
|||||||
|
|
||||||
# Add the top level "General" settings menu and entries
|
# Add the top level "General" settings menu and entries
|
||||||
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView))
|
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView))
|
||||||
|
|
||||||
|
# Scroll the general settings to the bottom
|
||||||
|
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView, dict(selected_button_option=settings_views.SettingsMenuView.VERSION), screenshot_name="SettingsMenuView_2"))
|
||||||
add_settings_entries(SettingsConstants.VISIBILITY__GENERAL)
|
add_settings_entries(SettingsConstants.VISIBILITY__GENERAL)
|
||||||
|
|
||||||
# Add the "Advanced" menu...
|
# Add the "Advanced" menu...
|
||||||
@@ -316,10 +326,25 @@ def generate_screenshots(locale):
|
|||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def mock_version_to_most_recent_release():
|
||||||
|
# Patch the Version get_* calls to the most recent release
|
||||||
|
with patch.multiple(Version,
|
||||||
|
get_version_name=Mock(return_value=latest_release_version_name),
|
||||||
|
get_version_fork=Mock(return_value="SeedSigner"),
|
||||||
|
get_version_timestamp=Mock(return_value=latest_release_version_timestamp),
|
||||||
|
get_short_commit_hash=Mock(return_value="abcd1234") # dummy value should be ignored
|
||||||
|
):
|
||||||
|
# Also have to mock us into SeedSigner OS
|
||||||
|
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
screenshot_sections = {
|
screenshot_sections = {
|
||||||
"Main Menu Views": [
|
"Main Menu Views": [
|
||||||
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True)),
|
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True), mock_context_manager=mock_version_to_most_recent_release),
|
||||||
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=False), screenshot_name="OpeningSplashView_no_partner_logos"),
|
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=False), screenshot_name="OpeningSplashView_no_partner_logos", mock_context_manager=mock_version_to_most_recent_release),
|
||||||
|
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True), screenshot_name="OpeningSplashView_current_git_state"),
|
||||||
ScreenshotConfig(MainMenuView),
|
ScreenshotConfig(MainMenuView),
|
||||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_removed', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED, activation_delay=0, duration=0)),
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_removed', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED, activation_delay=0, duration=0)),
|
||||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_inserted', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED, activation_delay=0, duration=0)),
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_inserted', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED, activation_delay=0, duration=0)),
|
||||||
@@ -445,6 +470,8 @@ def generate_screenshots(locale):
|
|||||||
"Settings Views": settings_views_list + [
|
"Settings Views": settings_views_list + [
|
||||||
ScreenshotConfig(settings_views.IOTestView),
|
ScreenshotConfig(settings_views.IOTestView),
|
||||||
ScreenshotConfig(settings_views.DonateView),
|
ScreenshotConfig(settings_views.DonateView),
|
||||||
|
ScreenshotConfig(settings_views.VersionView, mock_context_manager=mock_version_to_most_recent_release),
|
||||||
|
ScreenshotConfig(settings_views.VersionView, screenshot_name="VersionView_current_git_state"),
|
||||||
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"),
|
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"),
|
||||||
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"),
|
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"),
|
||||||
ScreenshotConfig(settings_views.SettingsSelectionRequiredWarningView, dict(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES)),
|
ScreenshotConfig(settings_views.SettingsSelectionRequiredWarningView, dict(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES)),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from seedsigner.helpers.version import Version, VersionUtils
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
CLI utility to extract the current version data and write to
|
||||||
|
`src/seedsigner/version.json`. Primarily used by the SeedSigner OS build process.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
* The SeedSigner OS build environment already relies on `git` being installed.
|
||||||
|
* This script can also be run in local dev but note slight difference in how
|
||||||
|
version_timestamp is gathered.
|
||||||
|
* Tries to pull current version status via `git` shell commands, but has fallbacks
|
||||||
|
to directly parse .git/ files.
|
||||||
|
|
||||||
|
Version data:
|
||||||
|
* version_name: Retrieves the current git status by checking, in order:
|
||||||
|
* Current git branch name
|
||||||
|
* Current git tag name
|
||||||
|
* Current git commit hash
|
||||||
|
|
||||||
|
* version_fork:
|
||||||
|
* The current repo owner.
|
||||||
|
|
||||||
|
* short_commit_hash:
|
||||||
|
* Current git short commit hash.
|
||||||
|
|
||||||
|
* version_timestamp:
|
||||||
|
* SeedSigner OS builder: Pulls last git commit time for the checked out
|
||||||
|
branch/tag/commit.
|
||||||
|
* Local dev: Scans the source python files for the most recent modified time.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# As soon as `Version` is instantiated, it will gather all the version data
|
||||||
|
# according to the logic in `VersionUtils`.
|
||||||
|
version_info = Version.get_instance().to_dict()
|
||||||
|
|
||||||
|
# Write the version.json file.
|
||||||
|
version_file_path = VersionUtils._get_version_file_path()
|
||||||
|
with open(version_file_path, "w") as f:
|
||||||
|
json.dump(version_info, f, indent=4)
|
||||||
|
|
||||||
|
print(f"Wrote version info to: {version_file_path}")
|
||||||
|
print(json.dumps(version_info, indent=4))
|
||||||
Reference in New Issue
Block a user