#!/usr/bin/env python # -*- coding: utf-8 -*- #*************************************************************************** #* * #* Copyright (c) 2015 Yorik van Havre * #* * #* This program is free software; you can redistribute it and/or modify * #* it under the terms of the GNU Lesser General Public License (LGPL) * #* as published by the Free Software Foundation; either version 2 of * #* the License, or (at your option) any later version. * #* for detail see the LICENCE text file. * #* * #* This program is distributed in the hope that it will be useful, * #* but WITHOUT ANY WARRANTY; without even the implied warranty of * #* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * #* GNU Library General Public License for more details. * #* * #* You should have received a copy of the GNU Library General Public * #* License along with this program; if not, write to the Free Software * #* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * #* USA * #* * #*************************************************************************** from __future__ import print_function __title__="FreeCAD Addon Manager Module" __author__ = "Yorik van Havre","Jonathan Wiedemann","Kurt Kremitzki" __url__ = "http://www.freecadweb.org" ''' FreeCAD Addon Manager Module It will fetch its contents from https://github.com/FreeCAD/FreeCAD-addons You need a working internet connection, and the GitPython package installed. ''' from PySide import QtCore, QtGui import sys, os, re, shutil import FreeCAD if sys.version_info.major < 3: import urllib2 else: import urllib.request as urllib2 ctx = None try: import ssl except: pass else: if hasattr(ssl,"create_default_context"): ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) NOGIT = False # for debugging purposes, set this to True to always use http downloads MACROS_BLACKLIST = ["BOLTS","WorkFeatures","how to install","PartsLibrary","FCGear"] # Qt tanslation handling try: _encoding = QtGui.QApplication.UnicodeUTF8 def translate(context, text, disambig=None): return QtGui.QApplication.translate(context, text, disambig, _encoding) except AttributeError: def translate(context, text, disambig=None): return QtGui.QApplication.translate(context, text, disambig) def symlink(source, link_name): if os.path.exists(link_name): print("symlink already exists") else: os_symlink = getattr(os, "symlink", None) if callable(os_symlink): os_symlink(source, link_name) else: import ctypes csl = ctypes.windll.kernel32.CreateSymbolicLinkW csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32) csl.restype = ctypes.c_ubyte flags = 1 if os.path.isdir(source) else 0 if csl(link_name, source, flags) == 0: raise ctypes.WinError() class AddonsInstaller(QtGui.QDialog): def __init__(self): QtGui.QDialog.__init__(self) self.repos = [] self.macros = [] self.setObjectName("AddonsInstaller") self.resize(326, 304) self.verticalLayout = QtGui.QVBoxLayout(self) self.tabWidget = QtGui.QTabWidget() self.verticalLayout.addWidget(self.tabWidget) self.listWorkbenches = QtGui.QListWidget() self.listWorkbenches.setIconSize(QtCore.QSize(16,16)) self.tabWidget.addTab(self.listWorkbenches,"") self.listMacros = QtGui.QListWidget() self.listMacros.setIconSize(QtCore.QSize(16,16)) self.tabWidget.addTab(self.listMacros,"") self.labelDescription = QtGui.QLabel() self.labelDescription.setMinimumSize(QtCore.QSize(0, 75)) self.labelDescription.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) self.labelDescription.setWordWrap(True) self.verticalLayout.addWidget(self.labelDescription) self.progressBar = QtGui.QProgressBar(self) #self.progressBar.setProperty("value", 24) self.progressBar.setObjectName("progressBar") #self.progressBar.hide() self.progressBar.setRange(0,0) self.verticalLayout.addWidget(self.progressBar) self.horizontalLayout = QtGui.QHBoxLayout() spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) self.horizontalLayout.addItem(spacerItem) self.buttonCheck = QtGui.QPushButton() icon = QtGui.QIcon.fromTheme("reload") self.buttonCheck.setIcon(icon) self.horizontalLayout.addWidget(self.buttonCheck) self.buttonCheck.hide() self.buttonInstall = QtGui.QPushButton() icon = QtGui.QIcon.fromTheme("download") self.buttonInstall.setIcon(icon) self.horizontalLayout.addWidget(self.buttonInstall) self.buttonRemove = QtGui.QPushButton() icon = QtGui.QIcon.fromTheme("edit-delete") self.buttonRemove.setIcon(icon) self.horizontalLayout.addWidget(self.buttonRemove) self.buttonCancel = QtGui.QPushButton() icon = QtGui.QIcon.fromTheme("cancel") self.buttonCancel.setIcon(icon) self.buttonCancel.setDefault(True) self.horizontalLayout.addWidget(self.buttonCancel) self.verticalLayout.addLayout(self.horizontalLayout) self.retranslateUi() QtCore.QObject.connect(self.buttonCancel, QtCore.SIGNAL("clicked()"), self.reject) QtCore.QObject.connect(self.buttonInstall, QtCore.SIGNAL("clicked()"), self.install) QtCore.QObject.connect(self.buttonRemove, QtCore.SIGNAL("clicked()"), self.remove) QtCore.QObject.connect(self.labelDescription, QtCore.SIGNAL("linkActivated(QString)"), self.showlink) QtCore.QObject.connect(self.listWorkbenches, QtCore.SIGNAL("currentRowChanged(int)"), self.show) QtCore.QObject.connect(self.tabWidget, QtCore.SIGNAL("currentChanged(int)"), self.switchtab) QtCore.QObject.connect(self.listMacros, QtCore.SIGNAL("currentRowChanged(int)"), self.show_macro) QtCore.QObject.connect(self.buttonCheck, QtCore.SIGNAL("clicked()"), self.check_updates) QtCore.QMetaObject.connectSlotsByName(self) self.update() if not NOGIT: try: import git except: self.buttonCheck.hide() else: self.buttonCheck.show() def retranslateUi(self): self.setWindowTitle(translate("AddonsInstaller","Addon manager")) self.labelDescription.setText(translate("AddonsInstaller", "Downloading addon list...")) self.buttonCheck.setToolTip(translate("AddonsInstaller", "Check for available updates")) self.buttonCancel.setText(translate("AddonsInstaller", "Close")) self.buttonInstall.setText(translate("AddonsInstaller", "Install / update")) self.buttonRemove.setText(translate("AddonsInstaller", "Remove")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.listWorkbenches), translate("AddonsInstaller", "Workbenches")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.listMacros), translate("AddonsInstaller", "Macros")) def update(self): self.listWorkbenches.clear() self.repos = [] self.info_worker = InfoWorker() self.info_worker.addon_repos.connect(self.update_repos) self.update_worker = UpdateWorker() self.update_worker.info_label.connect(self.set_information_label) self.update_worker.addon_repo.connect(self.add_addon_repo) self.update_worker.progressbar_show.connect(self.show_progress_bar) self.update_worker.start() def check_updates(self): if self.tabWidget.currentIndex() == 0: self.check_worker = CheckWBWorker(self.repos) self.check_worker.mark.connect(self.mark) self.check_worker.info_label.connect(self.set_information_label) self.check_worker.progressbar_show.connect(self.show_progress_bar) self.check_worker.start() def add_addon_repo(self, addon_repo): self.repos.append(addon_repo) if addon_repo[2] == 1 : self.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme("dialog-ok"),str(addon_repo[0]) + str(" (Installed)"))) else: self.listWorkbenches.addItem(" "+str(addon_repo[0])) def set_information_label(self, label): self.labelDescription.setText(label) def show(self,idx): if self.repos and idx >= 0: self.show_worker = ShowWorker(self.repos, idx) self.show_worker.info_label.connect(self.set_information_label) self.show_worker.addon_repos.connect(self.update_repos) self.show_worker.progressbar_show.connect(self.show_progress_bar) self.show_worker.start() def show_macro(self,idx): if self.macros and idx >= 0: self.showmacro_worker = ShowMacroWorker(self.macros, idx) self.showmacro_worker.info_label.connect(self.set_information_label) self.showmacro_worker.update_macro.connect(self.update_macro) self.showmacro_worker.progressbar_show.connect(self.show_progress_bar) self.showmacro_worker.start() def switchtab(self,idx): if idx == 1: if not self.macros: self.listMacros.clear() self.macros = [] self.macro_worker = MacroWorker() self.macro_worker.add_macro.connect(self.add_macro) self.macro_worker.info_label.connect(self.set_information_label) self.macro_worker.progressbar_show.connect(self.show_progress_bar) self.macro_worker.start() self.buttonCheck.setEnabled(False) else: self.buttonCheck.setEnabled(True) def update_repos(self, repos): self.repos = repos def add_macro(self, macro): self.macros.append(macro) if macro[1] == 1: self.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme("dialog-ok"),str(macro[0]) + str(" (Installed)"))) else: self.listMacros.addItem(" "+str(macro[0])) def update_macro(self, idx, macro): self.macros[idx] = macro def showlink(self,link): "opens a link with the system browser" #print("clicked: ",link) QtGui.QDesktopServices.openUrl(QtCore.QUrl(link, QtCore.QUrl.TolerantMode)) def install(self): if self.tabWidget.currentIndex() == 0: idx = self.listWorkbenches.currentRow() self.install_worker = InstallWorker(self.repos, idx) self.install_worker.info_label.connect(self.set_information_label) self.install_worker.progressbar_show.connect(self.show_progress_bar) self.install_worker.start() elif self.tabWidget.currentIndex() == 1: macropath = FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Macro').GetString("MacroPath",os.path.join(FreeCAD.ConfigGet("UserAppData"),"Macro")) if not os.path.isdir(macropath): os.makedirs(macropath) macro = self.macros[self.listMacros.currentRow()] if len(macro) < 5: self.labelDescription.setText(translate("AddonsInstaller", "Unable to install")) return macroname = "Macro_"+macro[0]+".FCMacro" macroname = macroname.replace(" ","_") macrofilename = os.path.join(macropath,macroname) macrofile = open(macrofilename,"wb") macrofile.write(macro[3]) macrofile.close() self.labelDescription.setText(translate("AddonsInstaller", "Macro successfully installed. The macro is now available from the Macros dialog.")) self.update_status() def show_progress_bar(self, state): if state == True: self.listWorkbenches.setEnabled(False) self.listMacros.setEnabled(False) self.buttonInstall.setEnabled(False) self.buttonRemove.setEnabled(False) self.buttonCheck.setEnabled(False) self.progressBar.show() else: self.progressBar.hide() self.listWorkbenches.setEnabled(True) self.listMacros.setEnabled(True) self.buttonInstall.setEnabled(True) self.buttonRemove.setEnabled(True) if self.tabWidget.currentIndex() == 0: self.buttonCheck.setEnabled(True) def remove(self): if self.tabWidget.currentIndex() == 0: idx = self.listWorkbenches.currentRow() basedir = FreeCAD.ConfigGet("UserAppData") moddir = basedir + os.sep + "Mod" clonedir = basedir + os.sep + "Mod" + os.sep + self.repos[idx][0] if os.path.exists(clonedir): shutil.rmtree(clonedir) self.labelDescription.setText(translate("AddonsInstaller", "Addon successfully removed. Please restart FreeCAD")) else: self.labelDescription.setText(translate("AddonsInstaller", "Unable to remove this addon")) elif self.tabWidget.currentIndex() == 1: macropath = FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Macro').GetString("MacroPath",os.path.join(FreeCAD.ConfigGet("UserAppData"),"Macro")) macro = self.macros[self.listMacros.currentRow()] if macro[1] != 1: return macroname = "Macro_"+macro[0]+".FCMacro" macroname = macroname.replace(" ","_") macrofilename = os.path.join(macropath,macroname) if os.path.exists(macrofilename): os.remove(macrofilename) self.labelDescription.setText(translate("AddonsInstaller", "Macro successfully removed.")) self.update_status() def update_status(self): self.listWorkbenches.clear() self.listMacros.clear() moddir = FreeCAD.ConfigGet("UserAppData") + os.sep + "Mod" macropath = FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Macro').GetString("MacroPath",os.path.join(FreeCAD.ConfigGet("UserAppData"),"Macro")) for wb in self.repos: if os.path.exists(os.path.join(moddir,wb[0])): self.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme("dialog-ok"),str(wb[0]) + str(" (Installed)"))) wb[2] = 1 else: self.listWorkbenches.addItem(" "+str(wb[0])) wb[2] = 0 for macro in self.macros: if os.path.exists(os.path.join(macropath,"Macro_"+macro[0].replace(" ","_")+".FCMacro")): self.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme("dialog-ok"),str(macro[0]) + str(" (Installed)"))) macro[1] = 1 else: self.listMacros.addItem(" "+str(macro[0])) macro[1] = 0 def mark(self,repo): for i in range(self.listWorkbenches.count()): w = self.listWorkbenches.item(i) if w.text().startswith(str(repo)): w.setText(str(repo) + str(" (Update available)")) w.setIcon(QtGui.QIcon.fromTheme("reload")) class UpdateWorker(QtCore.QThread): info_label = QtCore.Signal(str) addon_repo = QtCore.Signal(object) progressbar_show = QtCore.Signal(bool) def __init__(self): QtCore.QThread.__init__(self) def run(self): "populates the list of addons" self.progressbar_show.emit(True) if ctx: u = urllib2.urlopen("https://github.com/FreeCAD/FreeCAD-addons",context=ctx) else: u = urllib2.urlopen("https://github.com/FreeCAD/FreeCAD-addons") 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.ConfigGet("UserAppData") moddir = basedir + os.sep + "Mod" repos = [] for l in p: #name = re.findall("data-skip-pjax=\"true\">(.*?)<",l)[0] name = re.findall("title=\"(.*?) @",l)[0] self.info_label.emit(name) #url = re.findall("title=\"(.*?) @",l)[0] url = "https://github.com/" + re.findall("href=\"\/(.*?)\/tree",l)[0] addondir = moddir + os.sep + name #print ("found:",name," at ",url) if not os.path.exists(addondir): state = 0 else: state = 1 repos.append([name,url,state]) if not repos: self.info_label.emit(translate("AddonsInstaller", "Unable to download addon list.")) else: repos = sorted(repos, key=lambda s: s[0].lower()) for repo in repos: self.addon_repo.emit(repo) self.info_label.emit(translate("AddonsInstaller", "Workbenches list was updated.")) self.progressbar_show.emit(False) self.stop = True class InfoWorker(QtCore.QThread): addon_repos = QtCore.Signal(object) def __init__(self): QtCore.QThread.__init__(self) def run(self): i = 0 for repo in self.repos: url = repo[1] if ctx: u = urllib2.urlopen(url,context=ctx) else: u = urllib2.urlopen(url) p = u.read() if sys.version_info.major >= 3 and isinstance(p, bytes): p = p.decode("utf-8") u.close() desc = re.findall("",p) if desc: desc = desc[0] else: desc = "Unable to retrieve addon description" self.repos[i].append(desc) i += 1 self.addon_repos.emit(self.repos) self.stop = True class CheckWBWorker(QtCore.QThread): info_label = QtCore.Signal(str) mark = QtCore.Signal(str) progressbar_show = QtCore.Signal(bool) def __init__(self,repos): QtCore.QThread.__init__(self) self.repos = repos def run(self): if NOGIT: self.stop = True return try: import git except: self.stop = True return self.progressbar_show.emit(True) basedir = FreeCAD.ConfigGet("UserAppData") moddir = basedir + os.sep + "Mod" self.info_label.emit(translate("AddonsInstaller", "Checking for new versions...")) upds = 0 gitpython_warning = False for repo in self.repos: if repo[2] == 1: #installed self.info_label.emit(translate("AddonsInstaller","Checking repo")+" "+repo[0]+"...") clonedir = moddir + os.sep + repo[0] if os.path.exists(clonedir): 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: if not gitpython_warning: FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Outdated GitPython detected, consider upgrading with pip.\n")) gitpython_warning = True cw = bare_repo.config_writer() cw.set('core', 'bare', False) del cw repo = git.Repo(clonedir) repo.head.reset('--hard') gitrepo = git.Git(clonedir) gitrepo.fetch() if "git pull" in gitrepo.status(): self.mark.emit(repo[0]) upds += 1 self.progressbar_show.emit(False) if upds: self.info_label.emit(str(upds)+" "+translate("AddonsInstaller", "update(s) available")) else: self.info_label.emit(translate("AddonsInstaller","Everything is up to date")) self.stop = True class MacroWorker(QtCore.QThread): add_macro = QtCore.Signal(object) info_label = QtCore.Signal(str) progressbar_show = QtCore.Signal(bool) def __init__(self): QtCore.QThread.__init__(self) def run(self): "populates the list of addons" self.info_label.emit("Downloading list of macros...") self.progressbar_show.emit(True) macropath = FreeCAD.ParamGet('User parameter:BaseApp/Preferences/Macro').GetString("MacroPath",os.path.join(FreeCAD.ConfigGet("UserAppData"),"Macro")) if ctx: u = urllib2.urlopen("https://www.freecadweb.org/wiki/Macros_recipes",context=ctx) else: u = urllib2.urlopen("https://www.freecadweb.org/wiki/Macros_recipes") p = u.read() if sys.version_info.major >= 3 and isinstance(p, bytes): p = p.decode("utf-8") u.close() macros = re.findall("title=\"(Macro.*?)\"",p) macros = [mac for mac in macros if (not("translated" in mac))] macros.sort(key=str.lower) for mac in macros: macname = mac[6:] macname = macname.replace("&","&") if (not (macname in MACROS_BLACKLIST)) and (not("recipes" in macname.lower())): macfile = mac.replace(" ","_")+".FCMacro" if os.path.exists(os.path.join(macropath,macfile)): installed = 1 else: installed = 0 self.add_macro.emit([macname,installed]) self.info_label.emit(translate("AddonsInstaller", "List of macros successfully retrieved.")) self.progressbar_show.emit(False) self.stop = True class ShowWorker(QtCore.QThread): info_label = QtCore.Signal(str) addon_repos = QtCore.Signal(object) progressbar_show = QtCore.Signal(bool) def __init__(self, repos, idx): QtCore.QThread.__init__(self) self.repos = repos self.idx = idx def run(self): self.progressbar_show.emit(True) self.info_label.emit(translate("AddonsInstaller", "Retrieving description...")) if len(self.repos[self.idx]) == 4: desc = self.repos[self.idx][3] else: url = self.repos[self.idx][1] self.info_label.emit(translate("AddonsInstaller", "Retrieving info from ") + str(url)) if ctx: u = urllib2.urlopen(url,context=ctx) else: u = urllib2.urlopen(url) p = u.read() if sys.version_info.major >= 3 and isinstance(p, bytes): p = p.decode("utf-8") u.close() desc = re.findall("",p) if desc: desc = desc[0] else: desc = "Unable to retrieve addon description" self.repos[self.idx].append(desc) self.addon_repos.emit(self.repos) if self.repos[self.idx][2] == 1: upd = False # checking for updates if not NOGIT: try: import git except: pass else: repo = self.repos[self.idx] clonedir = FreeCAD.ConfigGet("UserAppData") + os.sep + "Mod" + os.sep + repo[0] if os.path.exists(clonedir): 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: FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Outdated GitPython detected, consider upgrading with pip.\n")) cw = bare_repo.config_writer() cw.set('core', 'bare', False) del cw repo = git.Repo(clonedir) repo.head.reset('--hard') gitrepo = git.Git(clonedir) gitrepo.fetch() if "git pull" in gitrepo.status(): upd = True if upd: message = "" + translate("AddonsInstaller", "An update is available for this addon.") + "
" + desc + ' - ' + self.repos[self.idx][1] + '' else: message = "" + translate("AddonsInstaller", "This addon is already installed.") + "
" + desc + ' - ' + self.repos[self.idx][1] + '' else: message = desc + ' - ' + self.repos[self.idx][1] + '' self.info_label.emit( message ) self.progressbar_show.emit(False) self.stop = True class ShowMacroWorker(QtCore.QThread): info_label = QtCore.Signal(str) update_macro = QtCore.Signal(int,object) progressbar_show = QtCore.Signal(bool) def __init__(self, macros, idx): QtCore.QThread.__init__(self) self.macros = macros self.idx = idx def run(self): self.progressbar_show.emit(True) self.info_label.emit(translate("AddonsInstaller", "Retrieving description...")) if len(self.macros[self.idx]) > 2: desc = self.macros[self.idx][2] url = self.macros[self.idx][4] else: mac = self.macros[self.idx][0].replace(" ","_") mac = mac.replace("&","%26") mac = mac.replace("+","%2B") url = "https://www.freecadweb.org/wiki/Macro_"+mac self.info_label.emit("Retrieving info from " + str(url)) if ctx: u = urllib2.urlopen(url,context=ctx) else: u = urllib2.urlopen(url) p = u.read() if sys.version_info.major >= 3 and isinstance(p, bytes): p = p.decode("utf-8") u.close() code = re.findall("
(.*?)<\/pre>",p.replace("\n","--endl--"))
            if code:
                code = code[0]
                code = code.replace("--endl--","\n")
            else:
                self.info_label.emit(translate("AddonsInstaller", "Unable to fetch the code of this macro."))
                self.progressbar_show.emit(False)
                self.stop = True
                return
            desc = re.findall("(.*?)<\/td>",p.replace("\n"," "))
            if desc:
                desc = desc[0]
            else:
                self.info_label.emit(translate("AddonsInstaller", "Unable to retrieve a description for this macro."))
                desc = "No description available"
            # clean HTML escape codes
            try:
                from HTMLParser import HTMLParser
            except ImportError:
                from html.parser import HTMLParser
            try:
                code = code.decode("utf8")
                code = HTMLParser().unescape(code)
                code = code.encode("utf8")
                code = code.replace("\xc2\xa0", " ")
            except:
                FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Unable to clean macro code: ")+mac+"\n")
            self.update_macro.emit(self.idx,self.macros[self.idx]+[desc,code,url])
        if self.macros[self.idx][1] == 1 :
            message = "" + translate("AddonsInstaller", "This addon is already installed.") + "
" + desc + ' - ' + url + '' else: message = desc + ' - ' + url + '' self.info_label.emit( message ) self.progressbar_show.emit(False) self.stop = True class InstallWorker(QtCore.QThread): info_label = QtCore.Signal(str) progressbar_show = QtCore.Signal(bool) def __init__(self, repos, idx): QtCore.QThread.__init__(self) self.idx = idx self.repos = repos def run(self): "installs or updates the selected addon" git = None try: import git except: self.info_label.emit("GitPython not found.") FreeCAD.Console.PrintWarning(translate("AddonsInstaller","GitPython not found. Using standard download instead.\n")) try: import zipfile except: self.info_label.emit("no zip support.") FreeCAD.Console.PrintError(translate("AddonsInstaller","Your version of python doesn't appear to support ZIP files. Unable to proceed.\n")) return try: import StringIO as io except ImportError: # StringIO is not available with python3 import io if self.idx < 0: return if not self.repos: return if NOGIT: git = None basedir = FreeCAD.ConfigGet("UserAppData") moddir = basedir + os.sep + "Mod" if not os.path.exists(moddir): os.makedirs(moddir) clonedir = moddir + os.sep + self.repos[self.idx][0] self.progressbar_show.emit(True) if os.path.exists(clonedir): self.info_label.emit("Updating module...") if git: if not os.path.exists(clonedir + os.sep + '.git'): # Repair addon installed with raw download bare_repo = git.Repo.clone_from(self.repos[self.idx][1], clonedir + os.sep + '.git', bare=True) try: with bare_repo.config_writer() as cw: cw.set('core', 'bare', False) except AttributeError: FreeCAD.Console.PrintWarning(translate("AddonsInstaller", "Outdated GitPython detected, consider upgrading with pip.\n")) cw = bare_repo.config_writer() cw.set('core', 'bare', False) del cw repo = git.Repo(clonedir) repo.head.reset('--hard') repo = git.Git(clonedir) answer = repo.pull() else: answer = self.download(self.repos[self.idx][1],clonedir) else: self.info_label.emit("Checking module dependencies...") depsok,answer = self.checkDependencies(self.repos[self.idx][1]) if depsok: if git: self.info_label.emit("Cloning module...") repo = git.Repo.clone_from(self.repos[self.idx][1], clonedir, branch='master') else: self.info_label.emit("Downloading module...") self.download(self.repos[self.idx][1],clonedir) answer = translate("AddonsInstaller", "Workbench successfully installed. Please restart FreeCAD to apply the changes.") # symlink any macro contained in the module to the macros folder macrodir = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro").GetString("MacroPath") for f in os.listdir(clonedir): if f.lower().endswith(".fcmacro"): symlink(clonedir+os.sep+f,macrodir+os.sep+f) FreeCAD.ParamGet('User parameter:Plugins/'+self.repos[self.idx][0]).SetString("destination",clonedir) answer += translate("AddonsInstaller", "A macro has been installed and is available the Macros menu") + ": " answer += f + "" self.info_label.emit(answer) self.progressbar_show.emit(False) self.stop = True def checkDependencies(self,baseurl): "checks if the repo contains a metadata.txt and check its contents" import FreeCADGui ok = True message = "" depsurl = baseurl.replace("github.com","raw.githubusercontent.com") if not depsurl.endswith("/"): depsurl += "/" depsurl += "master/metadata.txt" try: mu = urllib2.urlopen(depsurl) except urllib2.HTTPError: # no metadata.txt, we just continue without deps checking pass else: # metadata.txt found depsfile = mu.read() mu.close() deps = depsfile.split("\n") for l in deps: if l.startswith("workbenches="): depswb = l.split("=")[1].split(",") for wb in depswb: if wb.strip(): if not wb.strip() in FreeCADGui.listWorkbenches().keys(): if not wb.strip()+"Workbench" in FreeCADGui.listWorkbenches().keys(): ok = False message += translate("AddonsInstaller","Missing workbench") + ": " + wb + ", " elif l.startswith("pylibs="): depspy = l.split("=")[1].split(",") for pl in depspy: if pl.strip(): try: __import__(pl.strip()) except: ok = False message += translate("AddonsInstaller","Missing python module") +": " + pl + ", " elif l.startswith("optionalpylibs="): opspy = l.split("=")[1].split(",") for pl in opspy: if pl.strip(): try: __import__(pl.strip()) except: message += translate("AddonsInstaller","Missing optional python module (doesn't prevent installing)") +": " + pl + ", " if message and (not ok): message = translate("AddonsInstaller", "Some errors were found that prevent to install this workbench") + ": " + message + ". " message += translate("AddonsInstaller","Please install the missing components first.") return ok, message def download(self,giturl,clonedir): "downloads and unzip from github" import zipfile try: import StringIO as io except ImportError: # StringIO is not available with python3 import io bakdir = None if os.path.exists(clonedir): bakdir = clonedir+".bak" if os.path.exists(bakdir): shutil.rmtree(bakdir) os.rename(clonedir,bakdir) os.makedirs(clonedir) zipurl = giturl+"/archive/master.zip" try: print("Downloading "+zipurl) u = urllib2.urlopen(zipurl) except: return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl zfile = io.StringIO() zfile.write(u.read()) zfile = zipfile.ZipFile(zfile) master = zfile.namelist()[0] # github will put everything in a subfolder zfile.extractall(clonedir) u.close() zfile.close() for filename in os.listdir(clonedir+os.sep+master): shutil.move(clonedir+os.sep+master+os.sep+filename, clonedir+os.sep+filename) os.rmdir(clonedir+os.sep+master) if bakdir: shutil.rmtree(bakdir) return translate("AddonsInstaller", "Successfully installed") + " " + zipurl def launchAddonMgr(): # first use dialog readWarning = FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').GetBool('readWarning',False) if not readWarning: if QtGui.QMessageBox.warning(None,"FreeCAD",translate("AddonsInstaller", "The addons that can be installed here are not officially part of FreeCAD, and are not reviewed by the FreeCAD team. Make sure you know what you are installing!"), QtGui.QMessageBox.Cancel | QtGui.QMessageBox.Ok) != QtGui.QMessageBox.StandardButton.Cancel: FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').SetBool('readWarning',True) readWarning = True if readWarning: dialog = AddonsInstaller() dialog.exec_()