Skip to content

Commit

Permalink
Merge branch 'launcher_installer' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
themarpe committed Oct 21, 2021
2 parents e7ac84e + 128067e commit 1917446
Show file tree
Hide file tree
Showing 15 changed files with 739 additions and 0 deletions.
44 changes: 44 additions & 0 deletions .github/workflows/launcher.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: DepthAI Launcher CI/CD

on:
workflow_dispatch:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main

jobs:

windows-installer:
runs-on: windows-latest

steps:

# Clones repository without persisting credentials
# As this local git repository is packed with installer
- uses: actions/checkout@v2
with:
submodules: 'recursive'
persist-credentials: ''

# Inno Setup already installed in Windows2019 Image
# - name: Install Inno Setup
# run: .\launcher\windows\inno_setup.ps1

# Test if Inno Setup is already in PATH
# - name: Add Inno Setup to PATH
# run: echo "C:\Program Files (x86)\Inno Setup 6" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: Build Windows Installer
run: .\launcher\windows\build.ps1

- name: Upload Installer artifact
uses: actions/upload-artifact@v2
with:
name: installers
path: launcher/windows/build/Output/
retention-days: 5
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,6 @@ resources/*.json
*.whl

*.orig

# Virtual environment
virtualenv/
4 changes: 4 additions & 0 deletions launcher/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Ignore build directory
build/
# Ignore log file
log.dat
25 changes: 25 additions & 0 deletions launcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# DepthAI Launcher

DepthAI Launcher is a small utility that provides installation and updates for DepthAI Demo Application

## Windows

DepthAI Launcher includes installation setup for Windows (64bit only).
Installation carries an embedded Python distribution WinPython, DepthAI Launcher and `depthai` repository.

### Installer

In the following steps, building the Windows installer from source is presented.

#### Dependencies

The following dependencies are required
- Windows Host machine (x64)
- Inno Setup 6.2

#### Building

To build Windows installer, Inno Setup installation directory must be present in `PATH` environmental variable (`ISCC.exe` must be present in the directory).

Execute the `launcher/windows/build.ps1` script to create the Windows installer.
The built installer `DepthAI_setup.exe` can be found in `build/Output/`.
313 changes: 313 additions & 0 deletions launcher/launcher.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,313 @@
#/usr/bin/env python3

# Launcher for depthai_demo.py which provides updating capabilities

# Standard imports
import os, sys, subprocess, time, threading, argparse
from pathlib import Path
# Import splash screen
from splash_screen import SplashScreen
# Import version parser
from packaging import version
# PyQt5
from PyQt5 import QtCore, QtGui, QtWidgets

# Constants
SCRIPT_DIRECTORY=Path(os.path.abspath(os.path.dirname(__file__)))
DEPTHAI_DEMO_SCRIPT='depthai_demo.py'
DEPTHAI_INSTALL_REQUIREMENTS_SCRIPT='install_requirements.py'
DEFAULT_GIT_PATH='git'
DEPTHAI_REPOSITORY_NAME = 'depthai'
DEPTHAI_REMOTE_REPOSITORY_URL = 'https://github.com/luxonis/depthai.git'
LOG_FILE_PATH=Path(SCRIPT_DIRECTORY/'log.dat')

# Parse arguments
parser = argparse.ArgumentParser()
parser.add_argument('-r', '--repo', help='Path to DepthAI Git repository', default=SCRIPT_DIRECTORY/'..')
parser.add_argument('-g', '--git', help='Path to Git executable. Default: %(default)s', default=DEFAULT_GIT_PATH)
parser.add_argument('--disable-git', help='Disable git requirement and updating capability', default=False, action='store_true')
args = parser.parse_args()

pathToDepthaiRepository = args.repo
gitExecutable = args.git
if args.disable_git:
gitExecutable = ''

# Create a logger
class Logger(object):
def __init__(self):
self.terminal = sys.stdout
self.log = open(LOG_FILE_PATH, 'w')
def write(self, message):
self.terminal.write(message)
self.log.write(message)
def flush(self):
self.terminal.flush()
self.log.flush()
# Write both stdout and stderr to log files
# Note - doesn't work for subprocesses.
# Do proper fd dup for that case
logger = Logger()
sys.stdout = logger
sys.stderr = logger

qApp = QtWidgets.QApplication(['DepthAI Launcher'])
# Set style
#print(PyQt5.QtWidgets.QStyleFactory.keys())
#qApp.setStyle('Fusion')
# Set default Window icon
qApp.setWindowIcon(QtGui.QIcon(str(SCRIPT_DIRECTORY/'splash2.png')))
# Create splash screen
splashScreen = SplashScreen(str(SCRIPT_DIRECTORY/'splash2.png'))

def closeSplash():
splashScreen.hide()

class Worker(QtCore.QThread):
signalUpdateQuestion = QtCore.pyqtSignal(str, str)
sigInfo = QtCore.pyqtSignal(str, str)
sigCritical = QtCore.pyqtSignal(str, str)
sigWarning = QtCore.pyqtSignal(str, str)
# Should update if a new version is available?
shouldUpdate = True

@QtCore.pyqtSlot(str,str)
def updateQuestion(self, title, message):
ret = QtWidgets.QMessageBox.question(splashScreen, title, message, QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.Yes)
if ret == QtWidgets.QMessageBox.Yes:
self.shouldUpdate = True
return True
else:
self.shouldUpdate = False
return False

@QtCore.pyqtSlot(str,str)
def showInformation(self, title, message):
QtWidgets.QMessageBox.information(splashScreen, title, message)

@QtCore.pyqtSlot(str,str)
def showWarning(self, title, message):
QtWidgets.QMessageBox.warning(splashScreen, title, message)

@QtCore.pyqtSlot(str,str)
def showCritical(self, title, message):
QtWidgets.QMessageBox.critical(splashScreen, title, message)

def __init__(self, parent = None):
QtCore.QThread.__init__(self, parent)
self.signalUpdateQuestion[str, str].connect(self.updateQuestion, QtCore.Qt.BlockingQueuedConnection)
self.sigInfo[str, str].connect(self.showInformation, QtCore.Qt.BlockingQueuedConnection)
self.sigCritical[str, str].connect(self.showCritical, QtCore.Qt.BlockingQueuedConnection)
self.sigWarning[str, str].connect(self.showWarning, QtCore.Qt.BlockingQueuedConnection)
def __del__(self):
self.exiting = True
try:
self.wait()
except:
pass

def run(self):

try:

# New version available?
newVersionAvailable = False
# Current version name
currentVersion = 'Unknown'
newVersion = 'Unknown'
newVersionTag = 'vUnknown'
lastCall = ''

try:

# Check if 'disable git' option was specified
if gitExecutable != '':


# Check if repository exists
if os.path.isdir(pathToDepthaiRepository) and subprocess.run([gitExecutable, 'status'], cwd=pathToDepthaiRepository).returncode == 0:
pass
else:
# DepthAI repo not available, clone first
splashScreen.updateSplashMessage('Cloning DepthAI Repository ...')
splashScreen.enableHeartbeat(True)
# Repository doesn't exists, clone first
subprocess.check_call([gitExecutable, 'clone', DEPTHAI_REMOTE_REPOSITORY_URL, DEPTHAI_REPOSITORY_NAME], cwd=SCRIPT_DIRECTORY)

# Fetch changes
# Save error of an possible no internet connection scenario
lastCall = subprocess.run([gitExecutable, 'fetch'], cwd=pathToDepthaiRepository, stderr=subprocess.PIPE)
lastCall.check_returncode()

# Get all available versions
availableDepthAIVersions = []
proc = subprocess.Popen([gitExecutable, 'tag', '-l'], cwd=pathToDepthaiRepository, stdout=subprocess.PIPE)
while True:
line = proc.stdout.readline()
if not line:
break
# Check that the tag refers to DepthAI demo and not SDK
tag = line.rstrip().decode()
# Check that tag is actually a version
if type(version.parse(tag)) is version.Version:
availableDepthAIVersions.append(tag)
print(f'Available DepthAI versions: {availableDepthAIVersions}')

# If any available versions
if len(availableDepthAIVersions) == 0:
raise RuntimeError('No available depthai versions found')

# Assuming versions are available
# Get latest version
newVersionTag = availableDepthAIVersions[0]
newVersion = str(version.parse(newVersionTag))
for ver in availableDepthAIVersions:
if version.parse(ver) > version.parse(newVersionTag):
newVersionTag = ver
newVersion = str(version.parse(ver))

# Check current tag
ret = subprocess.run([gitExecutable, 'describe', '--tags'], cwd=pathToDepthaiRepository, stdout=subprocess.PIPE, check=True)
tag = ret.stdout.decode()
# See if its DepthAI version tag (if not, then suggest to update)
if len(tag.split('-')) == 1:
currentVersion = 'Unknown'
if type(version.parse(tag)) is version.Version:
print(f'Current tag: {tag}, ver: {str(version.parse(tag))}')
currentVersion = str(version.parse(tag))

# Check if latest version is newer than current
if version.parse(newVersionTag) > version.parse(tag):
newVersionAvailable = True
else:
newVersionAvailable = False

else:
newVersionAvailable = True
else:
newVersionAvailable = True

# If a new version is available, ask to update
if newVersionAvailable == True:
# Ask user whether to update
# Update by default
title = 'Update Available'
message = f'Version {newVersion} is available.\nCurrent version is {currentVersion}\nUpdate?'
print(f'Message Box ({title}): {message}')
self.signalUpdateQuestion.emit(title, message)

print(f'Should update? {self.shouldUpdate}')

if self.shouldUpdate == True:
# DepthAI repo not available, clone first
splashScreen.updateSplashMessage('Updating DepthAI Repository ...')
splashScreen.enableHeartbeat(True)
lastCall = subprocess.run([gitExecutable, 'checkout', newVersionTag], cwd=pathToDepthaiRepository, stderr=subprocess.PIPE)
if lastCall.returncode != 0:
# Stop animation
splashScreen.updateSplashMessage('')
splashScreen.enableHeartbeat(False)
# Couldn't update. Issue a warning
errMessage = lastCall.stderr.decode()
title = 'Update Aborted'
message = f'DepthAI Repository has uncommited changes. Update was aborted.\n{errMessage}'
print(f'Message Box ({title}): {message}')
self.sigWarning.emit(title, message)
else:
# present message of installing dependencies
splashScreen.updateSplashMessage('Installing DepthAI Requirements ...')
splashScreen.enableHeartbeat(True)

# Install requirements for depthai_demo.py
subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_INSTALL_REQUIREMENTS_SCRIPT}'], cwd=pathToDepthaiRepository)

except subprocess.CalledProcessError as ex:
errMessage = lastCall.stderr.decode()
title = 'Git Error'
message = f'Git produced the following error: {ex}\nOutput: {errMessage}'
print(f'Message Box ({title}): {message}')
#self.sigInfo.emit(title, message)
#raise Exception('Git Error')
except FileNotFoundError as ex:
# Stop animation
splashScreen.updateSplashMessage('')
splashScreen.enableHeartbeat(False)
title = 'No Git Available'
message = 'Git cannot be found in the path. Make sure Git is installed and added to the path, then try again'
print(f'Message Box ({title}): {message}')
# TODO(themarpe) - could be made optional, if the following raise and message
self.sigCritical.emit(title, message)
raise Exception('No Git Found')
except RuntimeError as ex:
# Stop animation
splashScreen.updateSplashMessage('')
splashScreen.enableHeartbeat(False)
title = 'No DepthAI Versions Found'
message = "Couldn't find any available DepthAI versions. Continuing with existing version. Please report to developers."
print(f'Message Box ({title}): {message}')
# TODO(themarpe) - could be made optional, if the following raise and message
self.sigWarning.emit(title, message)

try:
# Set to quit splash screen a little after subprocess is ran
skipSplashQuitFirstTime = False
def removeSplash():
time.sleep(2.5)
if not skipSplashQuitFirstTime:
closeSplash()
quitThread = threading.Thread(target=removeSplash)
quitThread.start()

# All ready, run the depthai_demo.py as a separate process
ret = subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_DEMO_SCRIPT}'], cwd=pathToDepthaiRepository, stderr=subprocess.PIPE)

# Print out stderr first
sys.stderr.write(ret.stderr.decode())

# Retry if failed by an ModuleNotFoundError, by installing the requirements
if ret.returncode != 0 and ('ModuleNotFoundError' in str(ret.stderr) or 'Version mismatch' in str(ret.stderr)):
skipSplashQuitFirstTime = True
print(f'Dependency issue raised. Retrying by installing requirements and restarting demo.')

# present message of installing dependencies
splashScreen.updateSplashMessage('Installing DepthAI Requirements ...')
splashScreen.enableHeartbeat(True)

# Install requirements for depthai_demo.py
installReqCall = subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_INSTALL_REQUIREMENTS_SCRIPT}'], cwd=pathToDepthaiRepository, stderr=subprocess.PIPE)
if installReqCall.returncode != 0:
# Some error happened. Notify user
title = 'Error Installing DepthAI Requirements'
message = f"Couldn't install DepthAI requirements. Check internet connection and try again. Log available at: {LOG_FILE_PATH}"
print(f'Message Box ({title}): {message}')
self.sigCritical.emit(title, message)
raise Exception(title)

# Remove message and animation
splashScreen.updateSplashMessage('')
splashScreen.enableHeartbeat(False)

quitThread.join()
skipSplashQuitFirstTime = False
quitThread = threading.Thread(target=removeSplash)
quitThread.start()

# All ready, run the depthai_demo.py as a separate process
subprocess.run([sys.executable, f'{pathToDepthaiRepository}/{DEPTHAI_DEMO_SCRIPT}'], cwd=pathToDepthaiRepository)
except:
pass
finally:
quitThread.join()

except Exception as ex:
# Catch all for any kind of an error
print(f'Unknown error occured ({ex}), exiting...')
finally:
# At the end quit anyway
closeSplash()
splashScreen.close()
qApp.exit()

qApp.worker = Worker()
qApp.worker.start()
sys.exit(qApp.exec())
Binary file added launcher/logo_only_EBl_icon.ico
Binary file not shown.
Loading

0 comments on commit 1917446

Please sign in to comment.