Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
# -*- 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
You can’t perform that action at this time.