Last active
January 13, 2020 10:07
-
-
Save luzpaz/f3bfaaef8aaec9c66b96e0941f6ed7a5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
# FreeCAD Headless AddonManager test | |
# (c) 2019 FreeCAD community LGPL | |
""" | |
The module can be executed with: | |
./FreeCAD.AppImage -c <path_to_file> AddonManagerHeadless.py | |
Note: | |
In order to test updates, it possible to use git reset --hard HEAD~1 | |
which will rollback a repo 1 commit behind master. | |
""" | |
# TODO: | |
# * Add Macro support | |
# * Modularize AKA `import addonmanagerheadless` | |
# * Support checking dependencies (https://github.com/FreeCAD/FreeCAD-addons/issues/29) | |
# * Add custom repo support | |
# * Manually update user.cfg with git URL(s) | |
# * Add an interactive mode | |
# * Add verbose mode | |
# * Add quiet mode | |
# * Add unittest | |
# * Add ability to run unittests on FC | |
# * Add terminal color support | |
# * Add emoji support | |
# * Output columns so less scrollback is needed | |
# * Show progress of git pull (percentage as a progress bar) https://stackoverflow.com/a/38780917 | |
# * Support git branch swapping (https://github.com/FreeCAD/FreeCAD-addons/issues/151) | |
# * Skipping restart trick (https://github.com/FreeCAD/FreeCAD-addons/issues/29) | |
# * Proxy support (when it's implemented) | |
# * https://github.com/FreeCAD/FreeCAD-addons/issues/157 | |
# * https://github.com/FreeCAD/FreeCAD-addons/issues/57 | |
# * Security audit | |
# * Translations? https://github.com/FreeCAD/FreeCAD-addons/issues/68 | |
import os, re, shutil, stat, sys, tempfile | |
import git | |
# import emoji # for friendlier output (maybe test terminal first to see if color emojis are allowed?) | |
import FreeCAD | |
import addonmanager_utilities as utils | |
# ----------------------------------------------- | |
DEBUG = True # True/False to enable/disable debugging in this script | |
import logging; logging.basicConfig(level=logging.DEBUG) | |
logger = logging.getLogger() | |
logger.disabled = True # This deactivates all logging.debug() functions in script | |
if DEBUG: | |
logger.disabled = False # This activates all logging.debug() functions in script | |
import pdb # python debugger | |
import readline # allows for backspace deleting in debugger | |
# ----------------------------------------------- | |
# Blacklisted addons | |
MACROS_BLACKLIST = ["BOLTS", | |
"WorkFeatures", | |
"how to install", | |
"PartsLibrary", | |
"FCGear"] | |
# These addons will print an additional message informing the user | |
OBSOLETE = ["assembly2", | |
"drawing_dimensioning", | |
"cura_engine"] | |
NOGIT = False # for debugging purposes, set this to True to always use http downloads | |
requested = ['A2plus', 'sheetmetal', 'foobar'] # addons to install | |
# pdb.set_trace() # breakpoint | |
def ListRepo(): | |
u = utils.urlopen("https://github.com/FreeCAD/FreeCAD-addons") | |
if not u: | |
print("Unable to open URL") | |
quit() | |
p = u.read() | |
if sys.version_info.major >= 3 and isinstance(p, bytes): | |
p = p.decode("utf-8") | |
u.close() | |
p = p.replace("\n"," ") | |
p = re.findall("octicon-file-submodule(.*?)message",p) | |
basedir = FreeCAD.getUserAppDataDir() | |
moddir = basedir + os.sep + "Mod" | |
repos = [] | |
# querying official addons | |
for l in p: | |
#name = re.findall("data-skip-pjax=\"true\">(.*?)<",l)[0] | |
res = re.findall("title=\"(.*?) @",l) | |
if res: | |
name = res[0] | |
else: | |
print("AddonMananger: Debug: couldn't find title in",l) | |
continue | |
# Print repo name by itself | |
# print(name) | |
#url = re.findall("title=\"(.*?) @",l)[0] | |
url = utils.getRepoUrl(l) | |
if url: | |
addondir = moddir + os.sep + name | |
# DEBUG: print ("found:",name," at ",url) | |
if os.path.exists(addondir) and os.listdir(addondir): | |
# make sure the folder exists and it contains files! | |
state = 1 | |
else: | |
state = 0 | |
repos.append([name,url,state]) | |
# querying custom addons | |
# customaddons = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons").GetString("CustomRepositories","").split("\n") | |
# for url in customaddons: | |
# if url: | |
# name = url.split("/")[-1] | |
# if name.lower().endswith(".git"): | |
# name = name[:-4] | |
# addondir = moddir + os.sep + name | |
# if not os.path.exists(addondir): | |
# sfor repo in repos:tate = 0 | |
# else: | |
# state = 1 | |
# repos.append([name,url,state]) | |
if not repos: | |
print("Unable to download addon list.") | |
else: | |
repos = sorted(repos, key=lambda s: s[0].lower()) | |
print("List official available Addons:\n") | |
for repo in repos: | |
# addon installed | |
if repo[2] == 1: | |
# distinguish w/ preceding '+' and addon name | |
print(' +\t' + repo[0]) | |
# addon not installed | |
if repo[2] == 0: | |
# print uninstalled addons name | |
print('\t' + repo[0]) | |
# print a legend | |
# print('\nLegend:\n{0} = Installed\n'.format(emoji.emojize(':heavy_check_mark:'))) | |
print('\nLegend:\n+ = Installed\n') | |
# just show what addons are installed | |
print('Currently installed Addons:') | |
for repo in repos: | |
if repo[2] == 1: | |
print('\t'+repo[0]) | |
return(repos) | |
def CheckUpdateRepo(repos): | |
''' Check if Addon(s) has/have (an) update(s)''' | |
basedir = FreeCAD.getUserAppDataDir() | |
moddir = basedir + os.sep + "Mod" | |
# upds = [] # store updates | |
for repo in repos: | |
if repo[2] == 1: | |
print("Checking updates for",repo[0]) | |
clonedir = moddir + os.sep + repo[0] | |
# if os.path.exists(clonedir): | |
# # check if this addon was not installed with git | |
# if not os.path.exists(clonedir + os.sep + '.git'): | |
# # It wasn't, therefore repair addon installed with raw download | |
# # creating a git.Repo object to represent repo | |
# bare_repo = git.Repo.clone_from(repo[1], clonedir + os.sep + '.git', bare=True) | |
# try: | |
# with bare_repo.config_writer() as cw: | |
# cw.set('core', 'bare', False) | |
# except AttributeError: | |
# if not gitpython_warning: | |
# print('Outdated GitPython detected, consider upgrading with pip') | |
# gitpython_warning = True | |
# cw = bare_repo.config_writer() | |
# cw.set('core', 'bare', False) | |
# del cw | |
# repo = git.Repo(clonedir) | |
# repo.head.reset('--hard') | |
# clone the addon repo | |
gitrepo = git.Git(clonedir) | |
try: | |
# gitrepo.fetch() | |
gitrepo.status() | |
# TODO: (?) write a little regex for "Your branch is up to date" | |
# print(gitrepo.status()+'\n') | |
repos[repos.index(repo)][2] = 2 # mark as already installed AND already checked for updates | |
# pdb.set_trace() # breakpoint | |
except: | |
print("AddonManager: Unable to fetch git updates for repo",repo[0]) | |
else: | |
# pdb.set_trace() # breakpoint | |
# 'git status' will tell us if we need to 'git pull' | |
# note: print(gitrepo.status()) for troubleshooting | |
if "git pull" in gitrepo.status(): | |
print('\t' + repo[0] + ' ' + 'has an update') | |
status = re.findall(r"Your branch (.*)\,", gitrepo.status()) | |
# print how many commits we are behind master | |
print('\t\t' + repo[0] + ' ' + status[0] + '\n') | |
# upds.append(repo) # not sure this is necessary due to: | |
repos[repos.index(repo)][2] = 3 # mark as already installed AND already checked for updates AND update available | |
# pdb.set_trace() # breakpoint | |
return(repos) | |
def UpdateRepo(repos): | |
'''Updates the selected addon(s)''' | |
basedir = FreeCAD.getUserAppDataDir() | |
moddir = basedir + os.sep + "Mod" | |
if not os.path.exists(moddir): | |
os.makedirs(moddir) | |
for repo in repos: | |
if repo[2] == 3: # installed but has an update pending | |
clonedir = moddir + os.sep + repo[0] | |
if os.path.exists(clonedir): | |
print('\tUpdating ' + repo[0] + ' addon...') | |
# first check if the addon was originally downloaded as a zip | |
if not os.path.exists(clonedir + os.sep + '.git'): | |
# Repair addon installed with raw download | |
bare_repo = git.Repo.clone_from(repo[1], clonedir + os.sep + '.git', bare=True) | |
try: | |
with bare_repo.config_writer() as cw: | |
cw.set('core', 'bare', False) | |
except AttributeError: | |
print("Outdated GitPython detected, consider upgrading with pip.") | |
cw = bare_repo.config_writer() | |
cw.set('core', 'bare', False) | |
del cw | |
repo = git.Repo(clonedir) | |
repo.head.reset('--hard') | |
repo = git.Git(clonedir) | |
try: | |
answer = repo.pull() | |
except: | |
print("Error updating module ",repo[1]," - Please fix manually") | |
answer = repo.status() | |
print(answer) | |
else: | |
# Update the submodules for this repository | |
repo_sms = git.Repo(clonedir) | |
for submodule in repo_sms.submodules: | |
submodule.update(init=True, recursive=True) | |
else: | |
pass | |
return(repos) | |
def CheckRequested(repos, requested): | |
'''Test requested to be installed addon against official list''' | |
validAddons = [] | |
req = ' '.join(requested) | |
print("\nChecking {0} against official addons\n".format(req)) | |
for request in requested: | |
for repo in repos: | |
# if request[0] in repos[repos.index(request)][0]: | |
if repo[0] == request: | |
print('\t' + request + ' is an official valid Addon') | |
# store addon in a separate list | |
validAddons.append(repo) | |
# somehow notify user that an addon name was invalid | |
# else: | |
# print(request + ' is not a valid pofficial Addon') | |
# requested.remove(request) | |
# return | |
# for valid in validAddon | |
# Valid addon contains the workbenches that are to be installed | |
return(validAddons) | |
def InstallRepo(repos, validAddons): | |
'''installs selected addon(s)''' | |
basedir = FreeCAD.getUserAppDataDir() | |
moddir = basedir + os.sep + "Mod" | |
if not os.path.exists(moddir): | |
os.makedirs(moddir) | |
for addon in validAddons: | |
clonedir = moddir + os.sep + addon[0] | |
# pdb.set_trace() # breakpoint | |
if os.path.exists(clonedir): | |
print('Installing ' + addon[0] + ' addon...') | |
# first check if the addon was originally downloaded as a zip | |
if not os.path.exists(clonedir + os.sep + '.git'): | |
# Repair addon installed with raw download | |
bare_repo = git.Repo.clone_from(addon[1], clonedir + os.sep + '.git', bare=True) | |
try: | |
with bare_repo.config_writer() as cw: | |
cw.set('core', 'bare', False) | |
except AttributeError: | |
print("Outdated GitPython detected, consider upgrading with pip.") | |
cw = bare_repo.config_writer() | |
cw.set('core', 'bare', False) | |
del cw | |
repo = git.Repo(clonedir) | |
repo.head.reset('--hard') | |
repo = git.Git(clonedir) | |
try: | |
print('\tInstalling ' + clonedir + '\n') | |
answer = repo.pull() | |
except: | |
print("Error updating module ",addon[1]," - Please fix manually") | |
print("Location: " + clonedir + '\n') | |
answer = repo.status() | |
print(answer) | |
else: | |
# Update the submodules for this repository | |
repo_sms = git.Repo(clonedir) | |
for submodule in repo_sms.submodules: | |
submodule.update(init=True, recursive=True) | |
def RemoveRepo(): | |
"""uninstalls a macro or workbench""" | |
basedir = FreeCAD.getUserAppDataDir() | |
moddir = basedir + os.sep + "Mod" | |
clonedir = moddir + os.sep + repos[0] | |
if os.path.exists(clonedir): | |
shutil.rmtree(clonedir, onerror=self.remove_readonly) | |
print("Addon successfully removed. Please restart FreeCAD") | |
else: | |
print("Unable to remove this addon") | |
# Show progress bars for git activities <-- Unimplemnted | |
class Progress(git.remote.RemoteProgress): | |
def update(self, op_code, cur_count, max_count=None, message=''): | |
print(self._cur_line) | |
# def update(self, op_code, cur_count, max_count=None, message=''): | |
# print(op_code, cur_count, max_count, cur_count / (max_count or 100.0), message or "NO MESSAGE") | |
# end | |
if __name__ == '__main__': | |
logging.debug("{0}\n".format(sys.argv)) | |
if sys.argv[3] == "list": | |
ListRepo() | |
elif sys.argv[3] == "check": | |
# repos variable is the updated snapshot of our system | |
repos = ListRepo() | |
CheckUpdateRepo(repos) | |
elif sys.argv[3] == 'updateall': | |
repos = ListRepo() | |
repos = CheckUpdateRepo(repos) | |
repos = UpdateRepo(repos) | |
elif sys.argv[3] == "install": | |
if not sys.argv[4]: | |
print( | |
''' | |
Please specify valid and case-sensitive addons you want to install preceding after the --install flag. | |
For example: --install A2plus BIM' | |
''') | |
sys.exit() | |
elif sys.argv[4]: | |
requested = sys.argv[4:] | |
output = ' '.join(requested) | |
print('Addon(s) requested:\n\t{0}\n'.format(output)) | |
# first get updated snapshot of the addon system | |
repos = ListRepo() | |
# check if the addons requested are valid | |
validAddons = CheckRequested(repos, requested) | |
# install requested valid addons | |
InstallRepo(repos, validAddons) | |
else: | |
print('--install argv problem, not sure what happened') | |
sys.exit() | |
elif sys.argv[3] == "remove": | |
print('Unimplemented') | |
else: | |
print(\ | |
'''' | |
Choose a valid option: | |
list\t lists all official addons + which are currently installed | |
''') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment