diff --git a/docs/source/int_started.rst b/docs/source/int_started.rst index 9da44533..8ae9c4c6 100644 --- a/docs/source/int_started.rst +++ b/docs/source/int_started.rst @@ -239,8 +239,8 @@ run: your python executable followed by ``novelWriter.py``. It should look something like this: ``C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py`` -You can also run the ``make.py`` script to generate a single executable, or an installer. -See `Build and Install novelWriter`_ for more details or run: ``python make.py help``. +You can also run the ``setup.py`` script to generate a single executable, or an installer. +See `Build and Install novelWriter`_ for more details or run: ``python setup.py help``. .. _python.org: https://www.python.org/downloads/windows/ .. _Build and Install novelWriter: https://github.com/vkbo/novelWriter/blob/main/setup/README.md diff --git a/make.py b/make.py deleted file mode 100755 index 859ad8ca..00000000 --- a/make.py +++ /dev/null @@ -1,333 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -This make script is intended for building distributable packages of -novelWriter. These are either: - - * A single file executable named dist/novelWriter(.exe). This is a - quite slow option, and the file is fairly big. - * A single directory named dist/novelWriter with a novelWriter(.exe), - and all dependecies included. - * The latter can be combined with a build stage of a setup.exe file if - on Windows. This requires Inno Setup to be installed and in path. - -In addition, providing the pip otion will cause the script to try to -install all dependencies needed for runing the build, and for running -novelWriter itself. -""" - -import os -import sys -import shutil -import subprocess - -OS_NONE = 0 -OS_LINUX = 1 -OS_WIN = 2 -OS_DARWIN = 3 - -# =============================================================================================== # -# Package Installer -# =============================================================================================== # - -def installPackages(hostOS): - """Install package dependencies both for this script and for running - novelWriter itself. - """ - print("") - print("Installing Dependencies") - print("#######################") - print("") - - installQueue = ["pip", "pyinstaller", "-r requirements.txt"] - if hostOS == OS_DARWIN: - installQueue.append("pyobjc") - - pyCmd = [sys.executable, "-m"] - pipCmd = ["pip", "install", "--user", "--upgrade"] - for stepCmd in installQueue: - pkgCmd = stepCmd.split(" ") - try: - subprocess.call(pyCmd + pipCmd + pkgCmd) - except Exception as e: - print("Failed with error:") - print(str(e)) - sys.exit(1) - - return - -# =============================================================================================== # -# Run PyInstaller on Package -# =============================================================================================== # - -def freezePackage(buildWindowed, oneFile, makeSetup, hostOS): - """Run PyInstaller to freeze the packages. This assumes all - dependencies are already in place. - """ - import PyInstaller.__main__ # noqa: E402 - - print("") - print("Running PyInstaller") - print("###################") - print("") - - if hostOS == OS_WIN: - dotDot = ";" - else: - dotDot = ":" - - sys.modules["FixTk"] = None - instOpt = [ - "--name=novelWriter", - "--clean", - "--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"), - "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), - "--exclude-module=PyQt5.QtQml", - "--exclude-module=PyQt5.QtBluetooth", - "--exclude-module=PyQt5.QtDBus", - "--exclude-module=PyQt5.QtMultimedia", - "--exclude-module=PyQt5.QtMultimediaWidgets", - "--exclude-module=PyQt5.QtNetwork", - "--exclude-module=PyQt5.QtNetworkAuth", - "--exclude-module=PyQt5.QtNfc", - "--exclude-module=PyQt5.QtQuick", - "--exclude-module=PyQt5.QtQuickWidgets", - "--exclude-module=PyQt5.QtRemoteObjects", - "--exclude-module=PyQt5.QtSensors", - "--exclude-module=PyQt5.QtSerialPort", - "--exclude-module=PyQt5.QtSql", - "--exclude-module=FixTk", - "--exclude-module=tcl", - "--exclude-module=tk", - "--exclude-module=_tkinter", - "--exclude-module=tkinter", - "--exclude-module=Tkinter", - ] - - if buildWindowed: - instOpt.append("--windowed") - - if oneFile and not makeSetup: - instOpt.append("--onefile") - else: - instOpt.append("--onedir") - - instOpt.append("novelWriter.py") - - # Make sample.zip first - try: - subprocess.call([sys.executable, "setup.py", "sample"]) - except Exception as e: - print("Failed with error:") - print(str(e)) - sys.exit(1) - - PyInstaller.__main__.run(instOpt) - - if not oneFile: - # These files are not needed, and take up a fair bit of space. - delFiles = [] - if hostOS == OS_WIN: - delFiles = [ - "Qt5DBus.dll", - "Qt5Network.dll", - "Qt5Qml.dll", - "Qt5QmlModels.dll", - "Qt5Quick.dll", - "Qt5Quick3D.dll", - "Qt5Quick3DAssetImport.dll", - "Qt5Quick3DRender.dll", - "Qt5Quick3DRuntimeRender.dll", - "Qt5Quick3DUtils.dll", - "Qt5Sql.dll" - ] - elif hostOS == OS_LINUX: - delFiles = [ - "libQt5DBus.so.5", - "libQt5Network.so.5", - "libQt5Qml.so.5", - "libQt5QmlModels.so.5", - "libQt5Quick.so.5", - "libQt5Quick3D.so.5", - "libQt5Quick3DAssetImport.so.5", - "libQt5Quick3DRender.so.5", - "libQt5Quick3DRuntimeRender.so.5", - "libQt5Quick3DUtils.so.5", - "libQt5Sql.so.5" - ] - distDir = os.path.join(os.getcwd(), "dist", "novelWriter") - for delFile in delFiles: - delPath = os.path.join(distDir, delFile) - if os.path.isfile(delPath): - print("Deleting file: %s" % delPath) - os.unlink(delPath) - - print("") - print("Build Finished") - print("") - print("The novelWriter executable should be in the folder named 'dist'") - print("") - - return - -# =============================================================================================== # -# Inno Setup Builder -# =============================================================================================== # - -def innoSetup(): - """Run the Inno Setup tool to build a setup.exe file for Windows. - """ - print("") - print("Running Inno Setup") - print("##################") - print("") - - # Read the iss template - issData = "" - with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile: - issData = inFile.read() - - import nw # noqa: E402 - issData = issData.replace(r"%%version%%", nw.__version__) - issData = issData.replace(r"%%dir%%", os.getcwd()) - - with open("setup.iss", mode="w+") as outFile: - outFile.write(issData) - - try: - subprocess.call(["iscc", "setup.iss"]) - except Exception as e: - print("Inno Setup failed with error:") - print(str(e)) - sys.exit(1) - - return - -# =============================================================================================== # -# Clean Build and Dist Folders -# =============================================================================================== # - -def cleanInstall(): - """Recursively delete the 'build' and 'dist' folders. - """ - print("") - print("Cleaning up build environment ...") - - buildDir = os.path.join(os.getcwd(), "build") - if os.path.isdir(buildDir): - try: - shutil.rmtree(buildDir) - print("Deleted folder 'build'") - except Exception as e: - print("Error: Cannot delete 'build' folder.") - print(str(e)) - sys.exit(1) - else: - print("Folder 'build' not found") - - distDir = os.path.join(os.getcwd(), "dist") - if os.path.isdir(distDir): - try: - shutil.rmtree(distDir) - print("Deleted folder 'dist'") - except Exception as e: - print("Error: Cannot delete 'dist' folder.") - print(str(e)) - sys.exit(1) - else: - print("Folder 'dist' not found") - - print("") - - return - -# =============================================================================================== # -# Process Build Steps -# =============================================================================================== # - -if __name__ == "__main__": - """Parse command line options and run the commands. - """ - # Detect OS - if sys.platform.startswith("linux"): - hostOS = OS_LINUX - elif sys.platform.startswith("darwin"): - hostOS = OS_DARWIN - elif sys.platform.startswith("win32"): - hostOS = OS_WIN - elif sys.platform.startswith("cygwin"): - hostOS = OS_WIN - else: - hostOS = OS_NONE - - # Flags and Variables - buildWindowed = True - oneFile = False - makeSetup = False - doFreeze = False - - helpMsg = ( - "\n" - "novelWriter Make Tool\n" - "=====================\n" - "This tool provides build commands for distibuting novelWriter as a\n" - "package. The available options are as follows:\n" - "\n" - "help Print the help message.\n" - "freeze Freeze the package and produces a folder of all\n" - " dependencies using pyinstaller.\n" - "onefile Build a standalone executable with all dependencies\n" - " bundled. Implies 'freeze', cannot be used with 'setup'.\n" - "pip Run pip to install all package dependencies for\n" - " novelWriter and this build tool.\n" - "setup Build a setup.exe installer for Windows. This option\n" - " automaticall disables the 'onefile' option.\n" - "clean This will attempt to delete the 'build' and 'dist'\n" - " folders in the current folder.\n" - ) - - if "help" in sys.argv or len(sys.argv) <= 1: - print(helpMsg) - sys.exit(0) - - if not os.path.isfile(os.path.join(os.getcwd(), "novelWriter.py")): - print("Error: This script must be run in the root folder of novelWriter.") - sys.exit(1) - - if not os.path.isdir(os.path.join(os.getcwd(), "nw")): - print("Error: This script must be run in the root folder of novelWriter.") - sys.exit(1) - - if "clean" in sys.argv: - sys.argv.remove("clean") - cleanInstall() - - if "pip" in sys.argv: - sys.argv.remove("pip") - installPackages(hostOS) - - if "freeze" in sys.argv: - sys.argv.remove("freeze") - doFreeze = True - - if "onefile" in sys.argv: - sys.argv.remove("onefile") - doFreeze = True - oneFile = True - - if "setup" in sys.argv: - sys.argv.remove("setup") - if hostOS == OS_WIN: - oneFile = False - makeSetup = True - else: - print("Error: Argument 'setup' for Inno Setup is Windows only.") - sys.exit(1) - - if doFreeze: - freezePackage(buildWindowed, oneFile, makeSetup, hostOS) - - if makeSetup: - innoSetup() - -# END Main diff --git a/nw/config.py b/nw/config.py index 74e9c1cc..6f1f5957 100644 --- a/nw/config.py +++ b/nw/config.py @@ -269,10 +269,16 @@ class Config: logger.verbose("Config path: %s" % self.confPath) logger.verbose("Data path: %s" % self.dataPath) - self.confFile = self.appHandle+".conf" - self.lastPath = os.path.expanduser("~") - self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) - self.appRoot = os.path.join(self.appPath, os.path.pardir) + self.confFile = self.appHandle+".conf" + self.lastPath = os.path.expanduser("~") + self.appPath = getattr(sys, "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) + self.appRoot = os.path.abspath(os.path.join(self.appPath, os.path.pardir)) + + if self.appRoot.endswith(".pyz"): + self.appRoot = os.path.abspath(os.path.join(self.appRoot, os.path.pardir)) + self.appPath = self.appRoot + + # Assets self.assetPath = os.path.join(self.appPath, "assets") self.themeRoot = os.path.join(self.assetPath, "themes") self.dictPath = os.path.join(self.assetPath, "dict") diff --git a/setup.py b/setup.py index ed2d7402..8f405ef0 100755 --- a/setup.py +++ b/setup.py @@ -20,20 +20,89 @@ OS_WIN = 2 OS_DARWIN = 3 # =============================================================================================== # -# Qt Assistant Documentation Builder +# General # =============================================================================================== # +## +# Package Installer (pip) +## + +def installPackages(hostOS): + """Install package dependencies both for this script and for running + novelWriter itself. + """ + print("") + print("Installing Dependencies") + print("#######################") + print("") + + installQueue = ["pip", "-r requirements.txt"] + if hostOS == OS_DARWIN: + installQueue.append("pyobjc") + + pyCmd = [sys.executable, "-m"] + pipCmd = ["pip", "install", "--user", "--upgrade"] + for stepCmd in installQueue: + pkgCmd = stepCmd.split(" ") + try: + subprocess.call(pyCmd + pipCmd + pkgCmd) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + + return + +## +# Clean Build and Dist Folders (clean) +## + +def cleanInstall(): + """Recursively delete the 'build' and 'dist' folders. + """ + print("") + print("Cleaning up build environment ...") + + buildDir = os.path.join(os.getcwd(), "build") + if os.path.isdir(buildDir): + try: + shutil.rmtree(buildDir) + print("Deleted folder 'build'") + except Exception as e: + print("Error: Cannot delete 'build' folder.") + print(str(e)) + sys.exit(1) + else: + print("Folder 'build' not found") + + distDir = os.path.join(os.getcwd(), "dist") + if os.path.isdir(distDir): + try: + shutil.rmtree(distDir) + print("Deleted folder 'dist'") + except Exception as e: + print("Error: Cannot delete 'dist' folder.") + print(str(e)) + sys.exit(1) + else: + print("Folder 'dist' not found") + + print("") + + return + +# =============================================================================================== # +# Additional Buiilds +# =============================================================================================== # + +## +# Qt Assistant Documentation Builder (qthelp) +## + def buildQtDocs(): """This function will build the documentation as a Qt help file. The file is then copied into the nw/assets/help directory and can be included in builds. - - Depends on packages: - * pip install sphinx - * pip install sphinx-rtd-theme - * pip install sphinxcontrib-qthelp - - It also requires the qhelpgenerator to be available on the system. """ buildDir = os.path.join("docs", "build", "qthelp") helpDir = os.path.join("nw", "assets", "help") @@ -85,6 +154,13 @@ def buildQtDocs(): print("") if buildFail: print("Documentation build: FAILED") + print("") + print("Dependencies:") + print(" * pip install sphinx") + print(" * pip install sphinx-rtd-theme") + print(" * pip install sphinxcontrib-qthelp") + print("") + print("It also requires the qhelpgenerator to be available on the system.") sys.exit(1) else: print("Documentation build: OK") @@ -92,9 +168,9 @@ def buildQtDocs(): return -# =============================================================================================== # -# Sample Project ZIP File Builder -# =============================================================================================== # +## +# Sample Project ZIP File Builder (sample) +## def buildSampleZip(): """Bundle the sample project into a single zip file to be saved into @@ -133,9 +209,313 @@ def buildSampleZip(): return # =============================================================================================== # -# Create Launcher +# Python Packaging # =============================================================================================== # +## +# Make Simple Package (winpack) +## + +def makeSimplePackage(embedPython): + """Run zipapp to freeze the packages. This assumes zipapp and pip + are already installed. + """ + import urllib.request + import zipfile + import zipapp + + # Set Up Folder + # ============= + + if not os.path.isdir("dist"): + os.mkdir("dist") + + outDir = os.path.join("dist", "novelWriter") + zipDir = os.path.join("dist", "zipapp_temp") + libDir = os.path.join(outDir, "lib") + if os.path.isdir(zipDir): + shutil.rmtree(zipDir) + if os.path.isdir(outDir): + shutil.rmtree(outDir) + + os.mkdir(outDir) + os.mkdir(libDir) + + # Download Python Embeddable + # ========================== + + if embedPython: + print("") + print("# Adding Python Embeddable") + print("# ========================") + print("") + + pyVers = "%d.%d.%d" % (sys.version_info[:3]) + zipFile = "python-%s-embed-amd64.zip" % pyVers + pyZip = os.path.join("dist", zipFile) + if not os.path.isfile(pyZip): + pyUrl = f"https://www.python.org/ftp/python/{pyVers}/{zipFile}" + print("Downloading: %s" % pyUrl) + urllib.request.urlretrieve(pyUrl, pyZip) + + print("Extracting ...") + with zipfile.ZipFile(pyZip, "r") as inFile: + inFile.extractall(outDir) + + print("Done") + print("") + + # Make sample.zip + # =============== + + try: + buildSampleZip() + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + + # Copy Package Files + # ================== + + print("") + print("# Copying Package Files") + print("# =====================") + print("") + + copyList = ["CHANGELOG.md", "LICENSE.md", "requirements.txt"] + iconList = ["novelwriter.ico", "x-novelwriter-project.ico"] + cpIgnore = shutil.ignore_patterns("__pycache__") + + print("Copying: nw") + shutil.copytree("nw", os.path.join(zipDir, "nw"), ignore=cpIgnore) + for copyFile in copyList: + print("Copying: %s" % copyFile) + shutil.copy2(copyFile, os.path.join(outDir, copyFile)) + for iconFile in iconList: + print("Copying: %s" % iconFile) + shutil.copy2(os.path.join("setup", "icons", iconFile), os.path.join(outDir, iconFile)) + + # Move assets to outDir as it should not be packed with the rest + print("Copying: assets") + os.rename(os.path.join(zipDir, "nw", "assets"), os.path.join(outDir, "assets")) + + print("Writing: __main__.py") + with open(os.path.join(zipDir, "__main__.py"), mode="w") as outFile: + outFile.write( + "#!\"pythonw.exe\"\n" + "\n" + "import os\n" + "import sys\n" + "\n" + "sys.path.insert(\n" + " 0, os.path.abspath(\n" + " os.path.join(os.path.dirname(__file__), os.path.pardir, \"lib\")\n" + " )\n" + ")\n\n" + "if __name__ == \"__main__\":\n" + " import nw\n" + " nw.main()\n" + ) + print("") + + pyzFile = os.path.join(outDir, "novelWriter.pyz") + zipapp.create_archive(zipDir, target=pyzFile, interpreter="python3") + + # Install Dependencies + # ==================== + + print("") + print("# Installing Dependencies") + print("# =======================") + print("") + + sysCmd = [sys.executable] + sysCmd += "-m pip install -r requirements.txt --target".split() + sysCmd += [libDir] + try: + subprocess.call(sysCmd) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + + for subDir in os.listdir(libDir): + chkDir = os.path.join(libDir, subDir) + if os.path.isdir(chkDir) and chkDir.endswith(".dist-info"): + shutil.rmtree(chkDir) + + print("") + + # Remove Unneeded Library Files + # ============================= + + delQtLibs = [ + "opengl32sw.dll", + "Qt5DBus.dll", + "Qt5Designer.dll", + "Qt5Network.dll", + "Qt5OpenGL.dll", + "Qt5Qml.dll", + "Qt5QmlModels.dll", + "Qt5QmlWorkerScript.dll", + "Qt5Quick.dll", + "Qt5Quick3D.dll", + "Qt5Quick3DAssetImport.dll", + "Qt5Quick3DRender.dll", + "Qt5Quick3DRuntimeRender.dll", + "Qt5Quick3DUtils.dll", + "Qt5QuickControls2.dll", + "Qt5QuickParticles.dll", + "Qt5QuickShapes.dll", + "Qt5QuickTemplates2.dll", + "Qt5QuickTest.dll", + "Qt5QuickWidgets.dll", + "Qt5Sql.dll", + ] + qtLibDir = os.path.join(libDir, "PyQt5", "Qt", "bin") + for libName in delQtLibs: + delFile = os.path.join(qtLibDir, libName) + if os.path.isfile(delFile): + print("Deleting: %s" % delFile) + os.unlink(delFile) + + qmlDir = os.path.join(libDir, "PyQt5", "Qt", "qml") + if os.path.isdir(qmlDir): + shutil.rmtree(qmlDir) + + print("") + print("Done!") + print("") + + return + +## +# Run PyInstaller on Package (freeze, onefile) +## + +def freezePackage(buildWindowed, oneFile, makeSetup, hostOS): + """Run PyInstaller to freeze the packages. This assumes all + dependencies are already in place. + """ + try: + import PyInstaller.__main__ # noqa: E402 + except Exception: + print("ERROR: Package 'pyinstaller' is missing on this system") + sys.exit(1) + + print("") + print("Running PyInstaller") + print("###################") + print("") + + if hostOS == OS_WIN: + dotDot = ";" + else: + dotDot = ":" + + sys.modules["FixTk"] = None + instOpt = [ + "--name=novelWriter", + "--clean", + "--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"), + "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), + "--exclude-module=PyQt5.QtQml", + "--exclude-module=PyQt5.QtBluetooth", + "--exclude-module=PyQt5.QtDBus", + "--exclude-module=PyQt5.QtMultimedia", + "--exclude-module=PyQt5.QtMultimediaWidgets", + "--exclude-module=PyQt5.QtNetwork", + "--exclude-module=PyQt5.QtNetworkAuth", + "--exclude-module=PyQt5.QtNfc", + "--exclude-module=PyQt5.QtQuick", + "--exclude-module=PyQt5.QtQuickWidgets", + "--exclude-module=PyQt5.QtRemoteObjects", + "--exclude-module=PyQt5.QtSensors", + "--exclude-module=PyQt5.QtSerialPort", + "--exclude-module=PyQt5.QtSql", + "--exclude-module=FixTk", + "--exclude-module=tcl", + "--exclude-module=tk", + "--exclude-module=_tkinter", + "--exclude-module=tkinter", + "--exclude-module=Tkinter", + ] + + if buildWindowed: + instOpt.append("--windowed") + + if oneFile and not makeSetup: + instOpt.append("--onefile") + else: + instOpt.append("--onedir") + + instOpt.append("novelWriter.py") + + # Make sample.zip first + try: + buildSampleZip() + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + + PyInstaller.__main__.run(instOpt) + + if not oneFile: + # These files are not needed, and take up a fair bit of space. + delFiles = [] + if hostOS == OS_WIN: + delFiles = [ + "Qt5DBus.dll", + "Qt5Network.dll", + "Qt5Qml.dll", + "Qt5QmlModels.dll", + "Qt5Quick.dll", + "Qt5Quick3D.dll", + "Qt5Quick3DAssetImport.dll", + "Qt5Quick3DRender.dll", + "Qt5Quick3DRuntimeRender.dll", + "Qt5Quick3DUtils.dll", + "Qt5Sql.dll" + ] + elif hostOS == OS_LINUX: + delFiles = [ + "libQt5DBus.so.5", + "libQt5Network.so.5", + "libQt5Qml.so.5", + "libQt5QmlModels.so.5", + "libQt5Quick.so.5", + "libQt5Quick3D.so.5", + "libQt5Quick3DAssetImport.so.5", + "libQt5Quick3DRender.so.5", + "libQt5Quick3DRuntimeRender.so.5", + "libQt5Quick3DUtils.so.5", + "libQt5Sql.so.5" + ] + distDir = os.path.join(os.getcwd(), "dist", "novelWriter") + for delFile in delFiles: + delPath = os.path.join(distDir, delFile) + if os.path.isfile(delPath): + print("Deleting file: %s" % delPath) + os.unlink(delPath) + + print("") + print("Build Finished") + print("") + print("The novelWriter executable should be in the folder named 'dist'") + print("") + + return + +# =============================================================================================== # +# General Installers +# =============================================================================================== # + +## +# XDG Installation (xdg-install, launcher) +## + def xdgInstall(): """Will attempt to install icons and make a launcher. """ @@ -278,12 +658,51 @@ def xdgInstall(): return # =============================================================================================== # -# Process Jobs +# Windows Installers +# =============================================================================================== # + +## +# Inno Setup Builder (setup-exe, setup-pyz) +## + +def innoSetup(setupType): + """Run the Inno Setup tool to build a setup.exe file for Windows based on either a pyinstaller + freeze package (exe) or a zipapp package (pyz). + """ + print("") + print("Running Inno Setup") + print("##################") + print("") + + # Read the iss template + issData = "" + with open(os.path.join("setup", "win_setup_%s.iss" % setupType), mode="r") as inFile: + issData = inFile.read() + + import nw # noqa: E402 + issData = issData.replace(r"%%version%%", nw.__version__) + issData = issData.replace(r"%%dir%%", os.getcwd()) + + with open("setup.iss", mode="w+") as outFile: + outFile.write(issData) + + try: + subprocess.call(["iscc", "setup.iss"]) + except Exception as e: + print("Inno Setup failed with error:") + print(str(e)) + sys.exit(1) + + return + +# =============================================================================================== # +# Process Command Line # =============================================================================================== # if __name__ == "__main__": """Parse command line options and run the commands. """ + # Detect OS if sys.platform.startswith("linux"): hostOS = OS_LINUX @@ -300,30 +719,75 @@ if __name__ == "__main__": "\n" "novelWriter Setup Tool\n" "======================\n" - "This tool provides some additional setup commands for novelWriter.\n" "\n" - "help Print this help message.\n" - "qthelp Build the help documentation for use with the Qt Assistant.\n" - " Run before install to enable in the the installed version.\n" - "sample Build the sample project as a zip file.\n" - " Run before install to enable creating sample projects.\n" - "install Installs novelWriter to the system's Python install location.\n" - " Run as root or with sudo for system-wide install, or as\n" - " user for single user install.\n" - "xdg-install Install launcher and icons for freedesktop systems.\n" - " Run as root or with sudo for system-wide install, or as\n" - " user for single user install.\n" + "This tool provides setup and build commands for installing or distibuting novelWriter\n" + "as a package on Linux, Mac and Windows. The available options are as follows:\n" + "\n" + "General:\n" + "\n" + " help Print the help message.\n" + " pip Install all package dependencies for novelWriter using pip.\n" + " clean Will attempt to delete the 'build' and 'dist' folders.\n" + "\n" + "Additional Builds:\n" + "\n" + " qthelp Build the help documentation for use with the Qt Assistant. Run before\n" + " install to have local help enable in the the installed version.\n" + " sample Build the sample project as a zip file. Run before install to enable\n" + " creating sample projects in the in-app New Project Wizard.\n" + "\n" + "Python Packaging:\n" + "\n" + " pack-pyz Creates a pyz package in a folder with all dependencies using the\n" + " zipapp tool. On Windows, python embeddable is added to the folder.\n" + " freeze Freeze the package and produces a folder with all dependencies using\n" + " the pyinstaller tool. This option is not designed for a specific OS.\n" + " onefile Build a standalone executable with all dependencies bundled using the\n" + " pyinstaller tool. Implies 'freeze', cannot be used with 'setup-exe'.\n" + "\n" + "General Installers:\n" + "\n" + " install Installs novelWriter to the system's Python install location.\n" + " Run as root or with sudo for system-wide install, or as\n" + " user for single user install.\n" + " xdg-install Install launcher and icons for freedesktop systems.\n" + " Run as root or with sudo for system-wide install, or as\n" + " user for single user install.\n" + "\n" + "Windows Installers:\n" + "\n" + " setup-exe Build a Windows installer from a pyinstaller freeze package using Inno\n" + " Setup. This option automatically disables 'onefile'.\n" + " setup-pyz Build a Windows installer from a zipapp package using Inno Setup.\n" ) + # Flags and Variables + buildWindowed = True + oneFile = False + makeSetupExe = False + makeSetupPyz = False + doFreeze = False + simplePack = False + embedPython = False + + # General + # ======= + if "help" in sys.argv: sys.argv.remove("help") print(helpMsg) sys.exit(0) - if "launcher" in sys.argv: - sys.argv.remove("launcher") - print("The 'launcher' option has been replaced by 'xdg-install'.") - sys.exit(1) + if "pip" in sys.argv: + sys.argv.remove("pip") + installPackages(hostOS) + + if "clean" in sys.argv: + sys.argv.remove("clean") + cleanInstall() + + # Additional Builds + # ================= if "qthelp" in sys.argv: sys.argv.remove("qthelp") @@ -333,14 +797,79 @@ if __name__ == "__main__": sys.argv.remove("sample") buildSampleZip() + # Python Packaging + # ================ + + if "pack-pyz" in sys.argv: + sys.argv.remove("pack-pyz") + simplePack = True + if hostOS == OS_WIN: + embedPython = True + + if "freeze" in sys.argv: + sys.argv.remove("freeze") + doFreeze = True + + if "onefile" in sys.argv: + sys.argv.remove("onefile") + doFreeze = True + oneFile = True + + # General Installers + # ================== + + if "launcher" in sys.argv: + sys.argv.remove("launcher") + print("The 'launcher' command has been replaced by 'xdg-install'.") + sys.exit(1) + if "xdg-install" in sys.argv: sys.argv.remove("xdg-install") if hostOS == OS_WIN: - print("ERROR: xdg-install cannot be used on Windows") + print("ERROR: Command 'xdg-install' cannot be used on Windows") sys.exit(1) else: xdgInstall() + # Windows Installers + # ================== + + if "setup-exe" in sys.argv: + sys.argv.remove("setup-exe") + if hostOS == OS_WIN: + oneFile = False + makeSetupExe = True + makeSetupPyz = False + else: + print("Error: Command 'setup-exe' for Inno Setup is Windows only.") + sys.exit(1) + + if "setup-pyz" in sys.argv: + sys.argv.remove("setup-pyz") + if hostOS == OS_WIN: + makeSetupExe = False + makeSetupPyz = True + else: + print("Error: Command 'setup-pyz' for Inno Setup is Windows only.") + sys.exit(1) + + # Actions + # ======= + # For functions that are controlled by multiple flags, or need to be + # run in a specific order. + + if simplePack: + makeSimplePackage(embedPython) + + if doFreeze: + freezePackage(buildWindowed, oneFile, makeSetupExe, hostOS) + + if makeSetupExe: + innoSetup("exe") + + if makeSetupPyz: + innoSetup("pyz") + if len(sys.argv) <= 1: # Nothing more to do sys.exit(0) diff --git a/setup/README.md b/setup/README.md index 252f70cd..45aaf397 100644 --- a/setup/README.md +++ b/setup/README.md @@ -5,44 +5,48 @@ The root folder of the repository contains two scripts for setup and install: ## Script `setup.py` -The `setup.py` is a standard Python setup script with a couple of additional options: +The `setup.py` is a standard Python setup script with a couple of additional +options: -* `qthelp`: Will attempt to build a single file QtAssistand documentation file. - This requires the Qt tools to be installed on the local system, as well as the sphinx build tools - for the documentation. -* `sample`: Will create a `sample.zip` file in the `nw/assets` folder. - This is the file the New Project Wizard uses to generate an example project. - If novelWriter is run from source, this file is not needed. -* `xdg-install`: Will install novelWriter icons, mimetype, and desktop and menu launcher on Linux desktops. - the application. This should work on standard Linux desktops. - By default, this is installed for the current user. Run with `sudo` to install system-wide. +### General -To install novelWriter as a local Python package, run: -```bash -sudo python setup.py install -``` +`help` – Print the help message -## Script `make.py` +`pip` – Install all package dependencies for novelWriter using pip. -The `make.py` script provides a number of convenient options for building packages if novelWriter. +`clean` – Will attempt to delete the `build` and `dist` folders. -Usage: -```bash -python make.py [command] -``` +### Additional Builds -It currently accept the following commands: +`qthelp` – Build the help documentation for use with the Qt Assistant. Run +before install to have local help enable in the the installed version -* `help`: Print the help message. -* `freeze`: Freeze the package and produces a folder of all dependencies using pyinstaller. -* `onefile`: Build a standalone executable with all dependencies bundled. - Implies `freeze`, cannot be used with `setup`. -* `pip`: Run pip to install all package dependencies for novelWriter and this build tool. -* `setup`: Build a setup.exe installer for Windows. - This option automaticall disables the `onefile` option. -* `clean`: This will attempt to delete the `build` and `dist` folders in the current folder. +`sample` – Build the sample project as a zip file. Run before install to enable +creating sample projects in the in-app New Project Wizard. -For instance, to create a Windows installer, run: -```bash -python make.py freeze setup -``` +### Python Packaging + +`pack-pyz` – Creates a pyz package in a folder with all dependencies using the +zipapp tool. This option is intended for Windows deployment. + +`freeze` – Freeze the package and produces a folder with all dependencies using +the pyinstaller tool. This option is not designed for a specific OS. + +`onefile` – Build a standalone executable with all dependencies bundled using +the pyinstaller tool. Implies `freeze`, cannot be used with `setup-exe` + +### General Installers + +`install` – Installs novelWriter to the system's Python install location. Run +as root or with sudo for system-wide install, or as user for single user +install. + +`xdg-install` – Install launcher and icons for freedesktop systems. Run as root +or with sudo for system-wide install, or as user for single user install. + +### Windows Installers + +`setup-exe` – Build a Windows installer from a pyinstaller freeze package using +Inno Setup. This option automatically disables `onefile`. + +`setup-pyz` – Build a Windows installer from a zipapp package using Inno Setup. diff --git a/setup/win_setup.iss b/setup/win_setup_exe.iss similarity index 88% rename from setup/win_setup.iss rename to setup/win_setup_exe.iss index ba2c9990..bed7c5ed 100644 --- a/setup/win_setup.iss +++ b/setup/win_setup_exe.iss @@ -47,7 +47,6 @@ Source: "{#nwAppDir}\novelWriter\*"; DestDir: "{app}"; Flags: ignoreversion recu [Icons] Name: "{autoprograms}\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}" Name: "{autodesktop}\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"; Tasks: desktopicon -Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"; Tasks: quicklaunchicon [Run] Filename: "{app}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent @@ -56,5 +55,5 @@ Filename: "{app}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChang Root: HKA; Subkey: "Software\Classes\.nwx\OpenWithProgids"; ValueType: string; ValueName: "novelWriterProject.nwx"; ValueData: ""; Flags: uninsdeletevalue Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx"; ValueType: string; ValueName: ""; ValueData: "novelWriter Project File"; Flags: uninsdeletekey Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\assets\icons\x-novelwriter-project.ico" -Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\novelWriter.exe"" ""%1""" -Root: HKA; Subkey: "Software\Classes\Applications\novelWriter.exe\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: "" +Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#nwAppExeName}"" ""%1""" +Root: HKA; Subkey: "Software\Classes\Applications\{#nwAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: "" diff --git a/setup/win_setup_pyz.iss b/setup/win_setup_pyz.iss new file mode 100644 index 00000000..f198bc22 --- /dev/null +++ b/setup/win_setup_pyz.iss @@ -0,0 +1,59 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define nwAppDir "%%dir%%\dist" +#define nwAppName "novelWriter" +#define nwAppVersion "%%version%%" +#define nwAppPublisher "novelWriter" +#define nwAppURL "http://novelWriter.io" +#define nwAppExeName "novelWriter.pyz" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{459A75D0-951F-4932-9809-6002EC8E733E} +AppName={#nwAppName} +AppVersion={#nwAppVersion} +AppVerName={#nwAppName} {#nwAppVersion} +AppPublisher={#nwAppPublisher} +AppPublisherURL={#nwAppURL} +AppSupportURL={#nwAppURL} +AppUpdatesURL={#nwAppURL} +DefaultDirName={autopf}\{#nwAppName} +DisableProgramGroupPage=yes +; The [Icons] "quicklaunchicon" entry uses {userappdata} but its [Tasks] entry has a proper IsAdminInstallMode Check. +UsedUserAreasWarning=no +; Uncomment the following line to run in non administrative install mode (install for current user only.) +;PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +OutputDir={#nwAppDir} +OutputBaseFilename=novelwriter-{#nwAppVersion}-win10-amd64-setup +Compression=lzma +SolidCompression=yes +WizardStyle=modern +ArchitecturesInstallIn64BitMode=x64 +ChangesAssociations=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode + +[Files] +Source: "{#nwAppDir}\novelWriter\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{autoprograms}\{#nwAppName}"; Filename: "{app}\pythonw.exe"; Parameters: "{#nwAppExeName}"; IconFilename: "{app}\novelwriter.ico" +Name: "{autodesktop}\{#nwAppName}"; Filename: "{app}\pythonw.exe"; Parameters: "{#nwAppExeName}"; IconFilename: "{app}\novelwriter.ico"; Tasks: desktopicon; + +[Run] +Filename: "{app}\pythonw.exe"; Parameters: "{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + +[Registry] +Root: HKA; Subkey: "Software\Classes\.nwx\OpenWithProgids"; ValueType: string; ValueName: "novelWriterProject.nwx"; ValueData: ""; Flags: uninsdeletevalue +Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx"; ValueType: string; ValueName: ""; ValueData: "novelWriter Project File"; Flags: uninsdeletekey +Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\assets\icons\x-novelwriter-project.ico" +Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\pythonw.exe"" ""{app}\{#nwAppExeName}"" ""%1""" +Root: HKA; Subkey: "Software\Classes\Applications\{#nwAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: ""