diff --git a/make.py b/make.py deleted file mode 100755 index 55a909fd..00000000 --- a/make.py +++ /dev/null @@ -1,527 +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", "-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 - -# =============================================================================================== # -# Make Simple Package -# =============================================================================================== # - -def makeWindowsPackage(): - """Run zipapp to freeze the packages. This assumes zipapp and pip - are already installed. - """ - import urllib.request - import zipfile - - # Set Up Folder - # ============= - - if not os.path.isdir("dist"): - os.mkdir("dist") - - outDir = os.path.join("dist", "novelWriter") - libDir = os.path.join(outDir, "lib") - if os.path.isdir(outDir): - shutil.rmtree(outDir) - - os.mkdir(outDir) - os.mkdir(libDir) - - # Download Python Embeddable - # ========================== - - print("") - print("# Downloading Python Embeddable") - print("# =============================") - print("") - - pyUrl = "https://www.python.org/ftp/python/3.8.7/python-3.8.7-embed-amd64.zip" - pyZip = os.path.join(outDir, "python_embed.zip") - print("URL: %s" % pyUrl) - - urllib.request.urlretrieve(pyUrl, pyZip) - - print("Extracting ...") - with zipfile.ZipFile(pyZip, "r") as inFile: - inFile.extractall(outDir) - - os.unlink(pyZip) - print("") - - # Make sample.zip - # =============== - - try: - subprocess.call([sys.executable, "setup.py", "sample"]) - 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(outDir, "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)) - - print("Writing: novelWriter.pyw") - with open(os.path.join(outDir, "novelWriter.pyw"), 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(os.path.join(os.path.dirname(__file__), \"lib\"))\n" - ")\n\n" - "if __name__ == \"__main__\":\n" - " import nw\n" - " nw.main()\n" - ) - print("") - - # 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 - -# =============================================================================================== # -# Inno Setup Builder -# =============================================================================================== # - -def innoSetup(setupType): - """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_%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 - -# =============================================================================================== # -# 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 - makeSetupExe = False - makeSetupPyz = False - doFreeze = False - winPack = False - - helpMsg = ( - "\n" - "novelWriter Make Tool\n" - "=====================\n" - "\n" - "This tool provides build commands for distibuting novelWriter as a package on Linux and\n" - "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" - "Python Packaging:\n" - "\n" - " winpack Creates a pyz package in a folder with all dependencies using the zipapp\n" - " tool. This option is intended for Windows deployment.\n" - " freeze Freeze the package and produces a folder with all dependencies using the\n" - " 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" - "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" - ) - - 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 "winpack" in sys.argv: - sys.argv.remove("winpack") - winPack = True - - if "setup_exe" in sys.argv: - sys.argv.remove("setup_exe") - if hostOS == OS_WIN: - oneFile = False - makeSetupExe = True - makeSetupPyz = False - else: - print("Error: Argument 'setup' 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: Argument 'setup' for Inno Setup is Windows only.") - sys.exit(1) - - if winPack: - makeWindowsPackage() - - if doFreeze: - freezePackage(buildWindowed, oneFile, makeSetupExe, hostOS) - - if makeSetupExe: - innoSetup("exe") - - if makeSetupPyz: - innoSetup("pyz") - -# END Main diff --git a/setup.py b/setup.py index 9fc4bcf6..1a0fd3c2 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,297 @@ def buildSampleZip(): return # =============================================================================================== # -# Create Launcher +# Python Packaging # =============================================================================================== # +## +# Make Simple Package (winpack) +## + +def makeWindowsPackage(): + """Run zipapp to freeze the packages. This assumes zipapp and pip + are already installed. + """ + import urllib.request + import zipfile + + # Set Up Folder + # ============= + + if not os.path.isdir("dist"): + os.mkdir("dist") + + outDir = os.path.join("dist", "novelWriter") + libDir = os.path.join(outDir, "lib") + if os.path.isdir(outDir): + shutil.rmtree(outDir) + + os.mkdir(outDir) + os.mkdir(libDir) + + # Download Python Embeddable + # ========================== + + print("") + print("# Downloading Python Embeddable") + print("# =============================") + print("") + + pyUrl = "https://www.python.org/ftp/python/3.8.7/python-3.8.7-embed-amd64.zip" + pyZip = os.path.join(outDir, "python_embed.zip") + print("URL: %s" % pyUrl) + + urllib.request.urlretrieve(pyUrl, pyZip) + + print("Extracting ...") + with zipfile.ZipFile(pyZip, "r") as inFile: + inFile.extractall(outDir) + + os.unlink(pyZip) + 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(outDir, "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)) + + print("Writing: novelWriter.pyw") + with open(os.path.join(outDir, "novelWriter.pyw"), 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(os.path.join(os.path.dirname(__file__), \"lib\"))\n" + ")\n\n" + "if __name__ == \"__main__\":\n" + " import nw\n" + " nw.main()\n" + ) + print("") + + # 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 +642,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 +703,74 @@ 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" + " winpack Creates a pyz package in a folder with all dependencies using the\n" + " zipapp tool. This option is intended for Windows deployment.\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 + winPack = 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 +780,81 @@ if __name__ == "__main__": sys.argv.remove("sample") buildSampleZip() + # Python Packaging + # ================ + + if "winpack" in sys.argv: + sys.argv.remove("winpack") + if hostOS == OS_WIN: + winPack = True + else: + print("Error: Command 'winpack' is Windows only.") + sys.exit(1) + + 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 winPack: + makeWindowsPackage() + + 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..cef06c2e 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 + +`winpack` – 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.