Project/Project.py

branch
maintenance
changeset 6273
0daf79d65080
parent 6265
56bd09c4c297
child 6291
94e0e688dcad
diff -r 0a74c1efab70 -r 0daf79d65080 Project/Project.py
--- a/Project/Project.py	Mon Apr 02 12:04:56 2018 +0200
+++ b/Project/Project.py	Tue May 01 12:03:52 2018 +0200
@@ -22,7 +22,7 @@
 import zipfile
 
 from PyQt5.QtCore import pyqtSlot, QFile, QFileInfo, pyqtSignal, \
-    QCryptographicHash, QIODevice, QByteArray, QObject, Qt
+    QCryptographicHash, QIODevice, QByteArray, QObject, Qt, QProcess
 from PyQt5.QtGui import QCursor, QKeySequence
 from PyQt5.QtWidgets import QLineEdit, QToolBar, QDialog, QInputDialog, \
     QApplication, QMenu, QAction
@@ -101,6 +101,10 @@
     @signal lexerAssociationsChanged() emitted after the lexer associations
         have been changed
     @signal projectChanged() emitted to signal a change of the project
+    @signal appendStdout(str) emitted after something was received from
+        a QProcess on stdout
+    @signal appendStderr(str) emitted after something was received from
+        a QProcess on stderr
     """
     dirty = pyqtSignal(int)
     projectLanguageAdded = pyqtSignal(str)
@@ -139,9 +143,14 @@
     showMenu = pyqtSignal(str, QMenu)
     lexerAssociationsChanged = pyqtSignal()
     projectChanged = pyqtSignal()
+    appendStdout = pyqtSignal(str)
+    appendStderr = pyqtSignal(str)
     
     eols = [os.linesep, "\n", "\r", "\r\n"]
     
+    DefaultMake = "make"
+    DefaultMakefile = "makefile"
+    
     def __init__(self, parent=None, filename=None):
         """
         Constructor
@@ -170,6 +179,7 @@
         }
         
         self.vcsMenu = None
+        self.__makeProcess = None
         
         self.__initProjectTypes()
         
@@ -459,6 +469,14 @@
             "PACKAGERSPARMS": {},
             "DOCUMENTATIONPARMS": {},
             "OTHERTOOLSPARMS": {},
+            "MAKEPARAMS": {
+                "MakeEnabled": False,
+                "MakeExecutable": "",
+                "MakeFile": "",
+                "MakeTarget": "",
+                "MakeParameters": "",
+                "MakeTestOnly": True,
+            },
             "EOL": -1,
         }
         
@@ -536,6 +554,9 @@
             "README": "OTHERS",
             "README.*": "OTHERS",
             "*.e4p": "OTHERS",
+            "GNUmakefile": "OTHERS",
+            "makefile": "OTHERS",
+            "Makefile": "OTHERS",
         }
         
         # Sources
@@ -1401,13 +1422,14 @@
             fn = os.path.join(self.ppath, langFile)
             if os.path.exists(fn):
                 s2t(fn)
-        except EnvironmentError:
+        except EnvironmentError as err:
             E5MessageBox.critical(
                 self.ui,
                 self.tr("Delete translation"),
                 self.tr(
                     "<p>The selected translation file <b>{0}</b> could not be"
-                    " deleted.</p>").format(langFile))
+                    " deleted.</p><p>Reason: {1}</p>").format(
+                    langFile, str(err)))
             return
         
         self.removeLanguageFile(langFile)
@@ -1422,13 +1444,14 @@
                 fn = os.path.join(self.ppath, qmFile)
                 if os.path.exists(fn):
                     s2t(fn)
-            except EnvironmentError:
+            except EnvironmentError as err:
                 E5MessageBox.critical(
                     self.ui,
                     self.tr("Delete translation"),
                     self.tr(
                         "<p>The selected translation file <b>{0}</b> could"
-                        " not be deleted.</p>").format(qmFile))
+                        " not be deleted.</p><p>Reason: {1}</p>").format(
+                        qmFile, str(err)))
                 return
         
     def appendFile(self, fn, isSourceFile=False, updateModel=True):
@@ -2119,13 +2142,14 @@
                     "__pycache__", "{0}.*{1}".format(tail, ext))
                 for f in glob.glob(pat):
                     s2t(f)
-        except EnvironmentError:
+        except EnvironmentError as err:
             E5MessageBox.critical(
                 self.ui,
                 self.tr("Delete file"),
                 self.tr(
                     "<p>The selected file <b>{0}</b> could not be"
-                    " deleted.</p>").format(fn))
+                    " deleted.</p><p>Reason: {1}</p>").format(
+                    fn, str(err)))
             return False
         
         self.removeFile(fn)
@@ -2148,13 +2172,13 @@
                 send2trash(dn)
             except ImportError:
                 shutil.rmtree(dn, True)
-        except EnvironmentError:
+        except EnvironmentError as err:
             E5MessageBox.critical(
                 self.ui,
                 self.tr("Delete directory"),
                 self.tr(
                     "<p>The selected directory <b>{0}</b> could not be"
-                    " deleted.</p>").format(dn))
+                    " deleted.</p><p>Reason: {1}</p>").format(dn, str(err)))
             return False
         
         self.removeDirectory(dn)
@@ -2218,6 +2242,10 @@
                 self.pdata["PROJECTTYPE"] in ["E6Plugin"])
             self.addLanguageAct.setEnabled(
                 bool(self.pdata["TRANSLATIONPATTERN"]))
+            self.makeGrp.setEnabled(
+                self.pdata["MAKEPARAMS"]["MakeEnabled"])
+            self.menuMakeAct.setEnabled(
+                self.pdata["MAKEPARAMS"]["MakeEnabled"])
             
             self.projectAboutToBeCreated.emit()
             
@@ -2241,6 +2269,7 @@
                         .format(self.ppath))
                     self.vcs = self.initVCS()
                     return
+                
                 # create an empty __init__.py file to make it a Python package
                 # (only for Python and Python3)
                 if self.pdata["PROGLANGUAGE"] in \
@@ -2249,6 +2278,7 @@
                     f = open(fn, "w", encoding="utf-8")
                     f.close()
                     self.appendFile(fn, True)
+                
                 # create an empty main script file, if a name was given
                 if self.pdata["MAINSCRIPT"]:
                     if not os.path.isabs(self.pdata["MAINSCRIPT"]):
@@ -2259,6 +2289,18 @@
                     f = open(ms, "w")
                     f.close()
                     self.appendFile(ms, True)
+                
+                if self.pdata["MAKEPARAMS"]["MakeEnabled"]:
+                    mf = self.pdata["MAKEPARAMS"]["MakeFile"]
+                    if mf:
+                        if not os.path.isabs(mf):
+                            mf = os.path.join(self.ppath, mf)
+                    else:
+                        mf = os.path.join(self.ppath, Project.DefaultMakefile)
+                    f = open(mf, "w")
+                    f.close()
+                    self.appendFile(mf)
+                
                 tpd = os.path.join(self.ppath, self.translationsRoot)
                 if not self.translationsRoot.endswith(os.sep):
                     tpd = os.path.dirname(tpd)
@@ -2305,11 +2347,32 @@
                                 self.tr(
                                     "<p>The mainscript <b>{0}</b> could not"
                                     " be created.<br/>Reason: {1}</p>")
-                                .format(self.ppath, str(err)))
-                    self.appendFile(ms)
+                                .format(ms, str(err)))
+                    self.appendFile(ms, True)
                 else:
                     ms = ""
                 
+                if self.pdata["MAKEPARAMS"]["MakeEnabled"]:
+                    mf = self.pdata["MAKEPARAMS"]["MakeFile"]
+                    if mf:
+                        if not os.path.isabs(mf):
+                            mf = os.path.join(self.ppath, mf)
+                    else:
+                        mf = os.path.join(self.ppath, Project.DefaultMakefile)
+                    if not os.path.exists(mf):
+                        try:
+                            f = open(mf, "w")
+                            f.close()
+                        except EnvironmentError as err:
+                            E5MessageBox.critical(
+                                self.ui,
+                                self.tr("Create Makefile"),
+                                self.tr(
+                                    "<p>The makefile <b>{0}</b> could not"
+                                    " be created.<br/>Reason: {1}</p>")
+                                .format(mf, str(err)))
+                    self.appendFile(mf)
+                
                 # add existing files to the project
                 res = E5MessageBox.yesNo(
                     self.ui,
@@ -2567,6 +2630,27 @@
                 if os.path.exists(ms):
                     self.appendFile(ms)
             
+            if self.pdata["MAKEPARAMS"]["MakeEnabled"]:
+                mf = self.pdata["MAKEPARAMS"]["MakeFile"]
+                if mf:
+                    if not os.path.isabs(mf):
+                        mf = os.path.join(self.ppath, mf)
+                else:
+                    mf = os.path.join(self.ppath, Project.DefaultMakefile)
+                if not os.path.exists(mf):
+                    try:
+                        f = open(mf, "w")
+                        f.close()
+                    except EnvironmentError as err:
+                        E5MessageBox.critical(
+                            self.ui,
+                            self.tr("Create Makefile"),
+                            self.tr(
+                                "<p>The makefile <b>{0}</b> could not"
+                                " be created.<br/>Reason: {1}</p>")
+                            .format(mf, str(err)))
+                self.appendFile(mf)
+            
             if self.pdata["PROJECTTYPE"] != projectType:
                 # reinitialize filetype associations
                 self.initFileTypes()
@@ -2811,6 +2895,10 @@
                         self.pdata["PROJECTTYPE"] in ["E6Plugin"])
                     self.addLanguageAct.setEnabled(
                         bool(self.pdata["TRANSLATIONPATTERN"]))
+                    self.makeGrp.setEnabled(
+                        self.pdata["MAKEPARAMS"]["MakeEnabled"])
+                    self.menuMakeAct.setEnabled(
+                        self.pdata["MAKEPARAMS"]["MakeEnabled"])
                     
                     self.__model.projectOpened()
                     self.projectOpenedHooks.emit()
@@ -3008,13 +3096,21 @@
            not noSave:
             self.__writeDebugProperties(True)
         
-        # now save all open modified files of the project
         vm = e5App().getObject("ViewManager")
+        
+        # check dirty status of all project files first
+        for fn in vm.getOpenFilenames():
+            if self.isProjectFile(fn):
+                reset = vm.checkFileDirty(fn)
+                if not reset:
+                    # abort shutting down
+                    return False
+        
+        # close all project related editors
         success = True
         for fn in vm.getOpenFilenames():
             if self.isProjectFile(fn):
-                success &= vm.closeWindow(fn)
-        
+                success &= vm.closeWindow(fn, ignoreDirty=True)
         if not success:
             return False
         
@@ -3035,7 +3131,7 @@
             self.vcs = None
             e5App().getObject("PluginManager").deactivateVcsPlugins()
         
-        # now close all project related windows
+        # now close all project related tool windows
         self.__closeAllWindows()
         
         self.__initData()
@@ -3057,6 +3153,8 @@
         self.menuApidocAct.setEnabled(False)
         self.menuPackagersAct.setEnabled(False)
         self.pluginGrp.setEnabled(False)
+        self.makeGrp.setEnabled(False)
+        self.menuMakeAct.setEnabled(False)
         
         self.__model.projectClosed()
         self.projectClosedHooks.emit()
@@ -4031,6 +4129,37 @@
             self.__pluginCreateSnapshotArchives)
         self.actions.append(self.pluginSArchiveAct)
 
+        self.makeGrp = createActionGroup(self)
+
+        self.makeExecuteAct = E5Action(
+            self.tr('Execute Make'),
+            self.tr('&Execute Make'), 0, 0,
+            self.makeGrp, 'project_make_execute')
+        self.makeExecuteAct.setStatusTip(
+            self.tr("Perform a 'make' run."))
+        self.makeExecuteAct.setWhatsThis(self.tr(
+            """<b>Execute Make</b>"""
+            """<p>This performs a 'make' run to rebuild the configured"""
+            """ target.</p>"""
+        ))
+        self.makeExecuteAct.triggered.connect(self.__executeMake)
+        self.actions.append(self.makeExecuteAct)
+
+        self.makeTestAct = E5Action(
+            self.tr('Test for Changes'),
+            self.tr('&Test for Changes'), 0, 0,
+            self.makeGrp, 'project_make_test')
+        self.makeTestAct.setStatusTip(
+            self.tr("Question 'make', if a rebuild is needed."))
+        self.makeTestAct.setWhatsThis(self.tr(
+            """<b>Test for Changes</b>"""
+            """<p>This questions 'make', if a rebuild of the configured"""
+            """ target is necessary.</p>"""
+        ))
+        self.makeTestAct.triggered.connect(
+            lambda: self.__executeMake(questionOnly=True))
+        self.actions.append(self.makeTestAct)
+
         self.closeAct.setEnabled(False)
         self.saveAct.setEnabled(False)
         self.saveasAct.setEnabled(False)
@@ -4064,7 +4193,7 @@
         self.apidocMenu.setTearOffEnabled(True)
         self.debuggerMenu = QMenu(self.tr('Debugger'), menu)
         self.packagersMenu = QMenu(self.tr('Pac&kagers'), menu)
-        self.packagersMenu.setTearOffEnabled(True)
+        self.makeMenu = QMenu(self.tr('Make'), menu)
         
         self.__menus = {
             "Main": menu,
@@ -4077,6 +4206,7 @@
             "Apidoc": self.apidocMenu,
             "Debugger": self.debuggerMenu,
             "Packagers": self.packagersMenu,
+            "Make": self.makeMenu,
         }
         
         # connect the aboutToShow signals
@@ -4090,6 +4220,7 @@
         self.packagersMenu.aboutToShow.connect(self.__showContextMenuPackagers)
         self.sessionMenu.aboutToShow.connect(self.__showContextMenuSession)
         self.debuggerMenu.aboutToShow.connect(self.__showContextMenuDebugger)
+        self.makeMenu.aboutToShow.connect(self.__showContextMenuMake)
         menu.aboutToShow.connect(self.__showMenu)
         
         # build the show menu
@@ -4113,9 +4244,15 @@
         self.debuggerMenu.addActions(self.dbgActGrp.actions())
         
         # build the packagers menu
+        self.packagersMenu.setTearOffEnabled(True)
         self.packagersMenu.addActions(self.pluginGrp.actions())
         self.packagersMenu.addSeparator()
         
+        # build the make menu
+        self.makeMenu.setTearOffEnabled(True)
+        self.makeMenu.addActions(self.makeGrp.actions())
+        self.makeMenu.addSeparator()
+        
         # build the main menu
         menu.setTearOffEnabled(True)
         menu.addActions(self.actGrp1.actions())
@@ -4131,6 +4268,8 @@
         menu.addSeparator()
         menu.addActions(self.actGrp2.actions())
         menu.addSeparator()
+        self.menuMakeAct = menu.addMenu(self.makeMenu)
+        menu.addSeparator()
         self.menuDiagramAct = menu.addMenu(self.graphicsMenu)
         menu.addSeparator()
         self.menuCheckAct = menu.addMenu(self.checksMenu)
@@ -4155,6 +4294,7 @@
         self.menuDebuggerAct.setEnabled(False)
         self.menuApidocAct.setEnabled(False)
         self.menuPackagersAct.setEnabled(False)
+        self.menuMakeAct.setEnabled(False)
         
         self.menu = menu
         return menu
@@ -5258,3 +5398,185 @@
                 break
         
         return version
+    
+    #########################################################################
+    ## Below are methods implementing the 'make' support
+    #########################################################################
+    
+    def __showContextMenuMake(self):
+        """
+        Private slot called before the make menu is shown.
+        """
+        self.showMenu.emit("Make", self.makeMenu)
+    
+    def hasDefaultMakeParameters(self):
+        """
+        Public method to test, if the project contains the default make
+        parameters.
+        
+        @return flag indicating default parameter set
+        @rtype bool
+        """
+        return self.pdata["MAKEPARAMS"] == {
+            "MakeEnabled": False,
+            "MakeExecutable": "",
+            "MakeFile": "",
+            "MakeTarget": "",
+            "MakeParameters": "",
+            "MakeTestOnly": True,
+        }
+    
+    def isMakeEnabled(self):
+        """
+        Public method to test, if make is enabled for the project.
+        
+        @return flag indicating enabled make support
+        @rtype bool
+        """
+        return self.pdata["MAKEPARAMS"]["MakeEnabled"]
+    
+    @pyqtSlot()
+    def executeMake(self):
+        """
+        Public slot to execute a project specific make run (auto-run)
+        (execute or question).
+        """
+        self.__executeMake(
+            questionOnly=self.pdata["MAKEPARAMS"]["MakeTestOnly"],
+            interactive=False)
+    
+    @pyqtSlot()
+    def __executeMake(self, questionOnly=False, interactive=True):
+        """
+        Private method to execute a project specific make run.
+        
+        @param questionOnly flag indicating to ask make for changes only
+        @type bool
+        @param interactive flag indicating an interactive invocation (i.e.
+            through a menu action)
+        @type bool
+        """
+        if not self.pdata["MAKEPARAMS"]["MakeEnabled"] or \
+           self.__makeProcess is not None:
+            return
+        
+        if self.pdata["MAKEPARAMS"]["MakeExecutable"]:
+            prog = self.pdata["MAKEPARAMS"]["MakeExecutable"]
+        else:
+            prog = Project.DefaultMake
+        
+        args = []
+        if self.pdata["MAKEPARAMS"]["MakeParameters"]:
+            args.extend(Utilities.parseOptionString(
+                self.pdata["MAKEPARAMS"]["MakeParameters"]))
+        
+        if self.pdata["MAKEPARAMS"]["MakeFile"]:
+            args.append("--makefile={0}".format(
+                self.pdata["MAKEPARAMS"]["MakeFile"]))
+        
+        if questionOnly:
+            args.append("--question")
+        
+        if self.pdata["MAKEPARAMS"]["MakeTarget"]:
+            args.append(self.pdata["MAKEPARAMS"]["MakeTarget"])
+        
+        self.__makeProcess = QProcess(self)
+        self.__makeProcess.readyReadStandardOutput.connect(
+            self.__makeReadStdOut)
+        self.__makeProcess.readyReadStandardError.connect(
+            self.__makeReadStdErr)
+        self.__makeProcess.finished.connect(
+            lambda exitCode, exitStatus: self.__makeFinished(
+                exitCode, exitStatus, questionOnly, interactive))
+        self.__makeProcess.setWorkingDirectory(self.getProjectPath())
+        self.__makeProcess.start(prog, args)
+        
+        if not self.__makeProcess.waitForStarted():
+            E5MessageBox.critical(
+                self.ui,
+                self.tr("Execute Make"),
+                self.tr("""The make process did not start."""))
+            
+            self.__cleanupMake()
+    
+    @pyqtSlot()
+    def __makeReadStdOut(self):
+        """
+        Private slot to process process output received via stdout.
+        """
+        if self.__makeProcess is not None:
+            output = str(self.__makeProcess.readAllStandardOutput(),
+                         Preferences.getSystem("IOEncoding"),
+                         'replace')
+            self.appendStdout.emit(output)
+    
+    @pyqtSlot()
+    def __makeReadStdErr(self):
+        """
+        Private slot to process process output received via stderr.
+        """
+        if self.__makeProcess is not None:
+            error = str(self.__makeProcess.readAllStandardError(),
+                        Preferences.getSystem("IOEncoding"),
+                        'replace')
+            self.appendStderr.emit(error)
+    
+    def __makeFinished(self, exitCode, exitStatus, questionOnly,
+                       interactive=True):
+        """
+        Private slot handling the make process finished signal.
+        
+        @param exitCode exit code of the make process
+        @type int
+        @param exitStatus exit status of the make process
+        @type QProcess.ExitStatus
+        @param questionOnly flag indicating a test only run
+        @type bool
+        @param interactive flag indicating an interactive invocation (i.e.
+            through a menu action)
+        @type bool
+        """
+        if exitStatus == QProcess.CrashExit:
+            E5MessageBox.critical(
+                self.ui,
+                self.tr("Execute Make"),
+                self.tr("""The make process crashed."""))
+        else:
+            if questionOnly and exitCode == 1:
+                # a rebuild is needed
+                title = self.tr("Test for Changes")
+                
+                if self.pdata["MAKEPARAMS"]["MakeTarget"]:
+                    message = self.tr(
+                        """<p>There are changes that require the configured"""
+                        """ make target <b>{0}</b> to be rebuilt.</p>""")\
+                        .format(self.pdata["MAKEPARAMS"]["MakeTarget"])
+                else:
+                    message = self.tr(
+                        """<p>There are changes that require the default"""
+                        """ make target to be rebuilt.</p>""")
+                
+                if self.ui.notificationsEnabled() and not interactive:
+                    self.ui.showNotification(
+                        UI.PixmapCache.getPixmap("makefile48.png"),
+                        title,
+                        message)
+                else:
+                    E5MessageBox.information(self.ui, title, message)
+            elif exitCode > 1:
+                E5MessageBox.critical(
+                    self.ui,
+                    self.tr("Execute Make"),
+                    self.tr("""The makefile contains errors."""))
+        
+        self.__cleanupMake()
+    
+    def __cleanupMake(self):
+        """
+        Private method to clean up make related stuff.
+        """
+        self.__makeProcess.readyReadStandardOutput.disconnect()
+        self.__makeProcess.readyReadStandardError.disconnect()
+        self.__makeProcess.finished.disconnect()
+        self.__makeProcess.deleteLater()
+        self.__makeProcess = None

eric ide

mercurial