Skip to content

Instantly share code, notes, and snippets.

@luzpaz
Last active January 13, 2020 10:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save luzpaz/f3bfaaef8aaec9c66b96e0941f6ed7a5 to your computer and use it in GitHub Desktop.
Save luzpaz/f3bfaaef8aaec9c66b96e0941f6ed7a5 to your computer and use it in GitHub Desktop.
# -*- 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