build and upload executables created by PyInstaller
- create release on tag push
- build executables by PyInstaller:
- extract_otp_secrets_linux_x86_64 (glibc 2.28)
- extract_otp_secrets_win_x86_64.exe
- extract_otp_secrets_macos_x86_64 (untested)
- add --version
- build linux executable in docker container
- update README
- add TOC
- improve badges
- add PyInstaller section
- docker
- build BASE_IMAGE as ARG
- copy only required files to image
- add .alias
- build.sh
- fix clean
- fix generate results
- generate TOC
This commit is contained in:
@@ -38,11 +38,18 @@ import csv
|
||||
import fileinput
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse as urlparse
|
||||
from enum import Enum, IntEnum
|
||||
from typing import Any, List, Optional, TextIO, Tuple, Union
|
||||
from typing import Any, List, Optional, Sequence, TextIO, Tuple, Union
|
||||
|
||||
import colorama
|
||||
from pkg_resources import DistributionNotFound, get_distribution
|
||||
from qrcode import QRCode # type: ignore
|
||||
|
||||
import protobuf_generated_python.google_auth_pb2 as pb
|
||||
|
||||
# workaround for PYTHON <= 3.7: compatibility
|
||||
if sys.version_info >= (3, 8):
|
||||
@@ -50,16 +57,17 @@ if sys.version_info >= (3, 8):
|
||||
else:
|
||||
from typing_extensions import Final, TypedDict
|
||||
|
||||
from qrcode import QRCode # type: ignore
|
||||
# workaround for PYTHON <= 3.7: compatibility
|
||||
if sys.version_info >= (3, 8):
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
else:
|
||||
from importlib_metadata import PackageNotFoundError, version
|
||||
|
||||
import protobuf_generated_python.google_auth_pb2 as pb
|
||||
import colorama
|
||||
|
||||
debug_mode = '-d' in sys.argv[1:] or '--debug' in sys.argv[1:]
|
||||
|
||||
try:
|
||||
import cv2 # type: ignore # TODO use cv2 types if available
|
||||
|
||||
import numpy as np # TODO use numpy types if available
|
||||
|
||||
try:
|
||||
@@ -133,6 +141,8 @@ CAMERA: Final[str] = 'camera'
|
||||
verbose: IntEnum = LogLevel.NORMAL
|
||||
quiet: bool = False
|
||||
colored: bool = True
|
||||
executable: bool = False
|
||||
__version__: str
|
||||
|
||||
|
||||
def sys_main() -> None:
|
||||
@@ -140,6 +150,7 @@ def sys_main() -> None:
|
||||
|
||||
|
||||
def main(sys_args: list[str]) -> None:
|
||||
global executable
|
||||
# allow to use sys.stdout with with (avoid closing)
|
||||
sys.stdout.close = lambda: None # type: ignore
|
||||
# set encoding to utf-8, needed for Windows
|
||||
@@ -150,11 +161,15 @@ def main(sys_args: list[str]) -> None:
|
||||
# StringIO in tests do not have all attributes, ignore it
|
||||
pass
|
||||
|
||||
# https://pyinstaller.org/en/stable/runtime-information.html#run-time-information
|
||||
executable = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
|
||||
|
||||
args = parse_args(sys_args)
|
||||
|
||||
if colored:
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
if verbose >= LogLevel.DEBUG:
|
||||
print(f"Version: {get_full_version()}\n")
|
||||
if args.debug:
|
||||
sys.exit(0 if do_debug_checks() else 1)
|
||||
|
||||
@@ -237,15 +252,19 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count:
|
||||
|
||||
def parse_args(sys_args: list[str]) -> Args:
|
||||
global verbose, quiet, colored
|
||||
|
||||
# For PYTHON <= 3.7: Use :=
|
||||
name = os.path.basename(sys.argv[0])
|
||||
cmd = f"python {name}" if name.endswith('.py') else f"{name}"
|
||||
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
|
||||
if qreader_available:
|
||||
description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera."
|
||||
example_text = """examples:
|
||||
python extract_otp_secrets.py
|
||||
python extract_otp_secrets.py example_*.txt
|
||||
python extract_otp_secrets.py - < example_export.txt
|
||||
python extract_otp_secrets.py --csv - example_*.png | tail -n+2
|
||||
python extract_otp_secrets.py = < example_export.png"""
|
||||
example_text = f"""examples:
|
||||
{cmd}
|
||||
{cmd} example_*.txt
|
||||
{cmd} - < example_export.txt
|
||||
{cmd} --csv - example_*.png | tail -n+2
|
||||
{cmd} = < example_export.png"""
|
||||
|
||||
arg_parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=32),
|
||||
description=description_text,
|
||||
@@ -262,6 +281,7 @@ b) image file containing a QR code or = for stdin for an image containing a QR c
|
||||
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.ZBAR.name})', type=str, choices=[mode.name for mode in QRMode], default=QRMode.ZBAR.name)
|
||||
arg_parser.add_argument('-i', '--ignore', help='ignore duplicate otps', action='store_true')
|
||||
arg_parser.add_argument('--no-color', '-n', help='do not use ANSI colors in console output', action='store_true')
|
||||
arg_parser.add_argument('--version', '-V', help='print version and quit', action=PrintVersionAction)
|
||||
output_group = arg_parser.add_mutually_exclusive_group()
|
||||
output_group.add_argument('-d', '--debug', help='enter debug mode, do checks and quit', action='count')
|
||||
output_group.add_argument('-v', '--verbose', help='verbose output', action='count')
|
||||
@@ -731,6 +751,57 @@ def do_debug_checks() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class PrintVersionAction(argparse.Action):
|
||||
def __init__(self, option_strings: Sequence[str], dest: str, nargs: int = 0, **kwargs: Any) -> None:
|
||||
super().__init__(option_strings, dest, nargs, **kwargs)
|
||||
|
||||
def __call__(self, parser: argparse.ArgumentParser, namespace: Args, values: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None:
|
||||
print_version()
|
||||
parser.exit()
|
||||
|
||||
|
||||
def print_version() -> None:
|
||||
print(get_full_version())
|
||||
|
||||
|
||||
def get_full_version() -> str:
|
||||
version = get_raw_version()
|
||||
meta = [
|
||||
platform.python_implementation()
|
||||
]
|
||||
if executable: meta.append('exe')
|
||||
meta.append(f"called as {'package' if __package__ else 'script'}")
|
||||
return (
|
||||
f"extract_otp_secrets {version} {platform.system()} {platform.machine()}"
|
||||
f" Python {platform.python_version()}"
|
||||
f" ({'/'.join(meta)})"
|
||||
)
|
||||
|
||||
|
||||
# https://setuptools-git-versioning.readthedocs.io/en/stable/runtime_version.html
|
||||
def get_raw_version() -> str:
|
||||
global __version__
|
||||
|
||||
try:
|
||||
__version__ = version("extract_otp_secrets")
|
||||
return __version__
|
||||
except PackageNotFoundError:
|
||||
# package is not installed
|
||||
pass
|
||||
|
||||
# In some cases importlib cannot properly detect package version, for example it was compiled into executable file, so it uses some custom import mechanism.
|
||||
# Instead, use pkg_resources which is included in setuptools (but has a significant runtime cost)
|
||||
|
||||
try:
|
||||
__version__ = get_distribution("package-name").version
|
||||
return __version__
|
||||
except DistributionNotFound:
|
||||
# package is not installed
|
||||
pass
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def log_debug(*values: object, sep: Optional[str] = ' ') -> None:
|
||||
if colored:
|
||||
|
||||
Reference in New Issue
Block a user