From 799c833a662cde17c72d60d37519d745c6595ce6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Wed, 20 Jan 2021 18:25:51 +0100 Subject: [PATCH 1/7] Add a simple packaging function --- make.py | 130 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/make.py b/make.py index 859ad8ca..9acdeffc 100755 --- a/make.py +++ b/make.py @@ -170,6 +170,128 @@ def freezePackage(buildWindowed, oneFile, makeSetup, hostOS): return +# =============================================================================================== # +# Make Simple Package +# =============================================================================================== # + +def simplePackage(hostOS): + """Run zipapp to freeze the packages. This assumes zipapp and pip + are already installed. + """ + # import zipapp + + from nw import __version__ + + exName = f"novelwriter-{__version__}-zipapp" + + # Set Up Folder + # ============= + + if not os.path.isdir("dist"): + os.mkdir("dist") + + outDir = os.path.join("dist", exName) + libDir = os.path.join(outDir, "lib") + if os.path.isdir(outDir): + shutil.rmtree(outDir) + + os.mkdir(outDir) + os.mkdir(libDir) + + # Copy Package Files + # ================== + + copyList = ["CHANGELOG.md", "LICENSE.md", "requirements.txt"] + iconList = ["novelwriter.ico", "x-novelwriter-project.ico"] + cpIgnore = shutil.ignore_patterns("__pycache__") + + shutil.copytree("nw", os.path.join(outDir, "nw"), ignore=cpIgnore) + for copyFile in copyList: + shutil.copy2(copyFile, os.path.join(outDir, copyFile)) + for iconFile in iconList: + shutil.copy2(os.path.join("setup", "icons", iconFile), os.path.join(outDir, iconFile)) + + with open(os.path.join(outDir, "__main__.py"), mode="w") as outFile: + outFile.write( + "#!/usr/bin/env python3\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" + ) + + # Install Dependencies + # ==================== + + 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) + + # Remove Unneeded Library Files + # ============================= + + delQtLibs = [ + "Qt5DBus", + "Qt5Network", + "Qt5Qml", + "Qt5QmlModels", + "Qt5QmlWorkerScript", + "Qt5Quick", + "Qt5Quick3D", + "Qt5Quick3DAssetImport", + "Qt5Quick3DRender", + "Qt5Quick3DRuntimeRender", + "Qt5Quick3DUtils", + "Qt5QuickControls2", + "Qt5QuickParticles", + "Qt5QuickShapes", + "Qt5QuickTemplates2", + "Qt5QuickTest", + "Qt5QuickWidgets", + "Qt5Sql", + ] + qtLibDir = os.path.join(libDir, "PyQt5", "Qt", "lib") + for libName in delQtLibs: + if hostOS == OS_WIN: + libFile = f"{libName}.dll" + elif hostOS == OS_LINUX: + libFile = f"lib{libName}.so.5" + else: + continue + + delFile = os.path.join(qtLibDir, libFile) + 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) + + # zipapp.create_archive( + # outDir, + # target=os.path.join("dist", f"{exName}.pyz"), + # interpreter="/usr/bin/env python3" + # ) + + return + # =============================================================================================== # # Inno Setup Builder # =============================================================================================== # @@ -265,6 +387,7 @@ if __name__ == "__main__": oneFile = False makeSetup = False doFreeze = False + simPack = False helpMsg = ( "\n" @@ -315,6 +438,10 @@ if __name__ == "__main__": doFreeze = True oneFile = True + if "package" in sys.argv: + sys.argv.remove("package") + simPack = True + if "setup" in sys.argv: sys.argv.remove("setup") if hostOS == OS_WIN: @@ -324,6 +451,9 @@ if __name__ == "__main__": print("Error: Argument 'setup' for Inno Setup is Windows only.") sys.exit(1) + if simPack: + simplePackage(hostOS) + if doFreeze: freezePackage(buildWindowed, oneFile, makeSetup, hostOS) From d425cdc27dda277a14adf7cc40f7e17009da0f89 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 21 Jan 2021 00:50:13 +0100 Subject: [PATCH 2/7] Added alternative packaging method for windows --- make.py | 134 ++++++++++++++++++++++++++++---------------- setup/win_setup.iss | 15 +++-- 2 files changed, 94 insertions(+), 55 deletions(-) diff --git a/make.py b/make.py index 9acdeffc..a699ad13 100755 --- a/make.py +++ b/make.py @@ -174,15 +174,12 @@ def freezePackage(buildWindowed, oneFile, makeSetup, hostOS): # Make Simple Package # =============================================================================================== # -def simplePackage(hostOS): +def makeWindowsPackage(): """Run zipapp to freeze the packages. This assumes zipapp and pip are already installed. """ - # import zipapp - - from nw import __version__ - - exName = f"novelwriter-{__version__}-zipapp" + import urllib.request + import zipfile # Set Up Folder # ============= @@ -190,7 +187,7 @@ def simplePackage(hostOS): if not os.path.isdir("dist"): os.mkdir("dist") - outDir = os.path.join("dist", exName) + outDir = os.path.join("dist", "novelWriter") libDir = os.path.join(outDir, "lib") if os.path.isdir(outDir): shutil.rmtree(outDir) @@ -198,22 +195,63 @@ def simplePackage(hostOS): 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)) - with open(os.path.join(outDir, "__main__.py"), mode="w") as outFile: + print("Writing: novelWriter.pyw") + with open(os.path.join(outDir, "novelWriter.pyw"), mode="w") as outFile: outFile.write( - "#!/usr/bin/env python3\n" + "#!\"pythonw.exe\"\n" + "\n" "import os\n" "import sys\n" "\n" @@ -224,10 +262,16 @@ def simplePackage(hostOS): " 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] @@ -243,39 +287,37 @@ def simplePackage(hostOS): if os.path.isdir(chkDir) and chkDir.endswith(".dist-info"): shutil.rmtree(chkDir) + print("") + # Remove Unneeded Library Files # ============================= delQtLibs = [ - "Qt5DBus", - "Qt5Network", - "Qt5Qml", - "Qt5QmlModels", - "Qt5QmlWorkerScript", - "Qt5Quick", - "Qt5Quick3D", - "Qt5Quick3DAssetImport", - "Qt5Quick3DRender", - "Qt5Quick3DRuntimeRender", - "Qt5Quick3DUtils", - "Qt5QuickControls2", - "Qt5QuickParticles", - "Qt5QuickShapes", - "Qt5QuickTemplates2", - "Qt5QuickTest", - "Qt5QuickWidgets", - "Qt5Sql", + "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", "lib") + qtLibDir = os.path.join(libDir, "PyQt5", "Qt", "bin") for libName in delQtLibs: - if hostOS == OS_WIN: - libFile = f"{libName}.dll" - elif hostOS == OS_LINUX: - libFile = f"lib{libName}.so.5" - else: - continue - - delFile = os.path.join(qtLibDir, libFile) + delFile = os.path.join(qtLibDir, libName) if os.path.isfile(delFile): print("Deleting: %s" % delFile) os.unlink(delFile) @@ -284,11 +326,9 @@ def simplePackage(hostOS): if os.path.isdir(qmlDir): shutil.rmtree(qmlDir) - # zipapp.create_archive( - # outDir, - # target=os.path.join("dist", f"{exName}.pyz"), - # interpreter="/usr/bin/env python3" - # ) + print("") + print("Done!") + print("") return @@ -387,7 +427,7 @@ if __name__ == "__main__": oneFile = False makeSetup = False doFreeze = False - simPack = False + winPack = False helpMsg = ( "\n" @@ -438,9 +478,9 @@ if __name__ == "__main__": doFreeze = True oneFile = True - if "package" in sys.argv: - sys.argv.remove("package") - simPack = True + if "winpack" in sys.argv: + sys.argv.remove("winpack") + winPack = True if "setup" in sys.argv: sys.argv.remove("setup") @@ -451,8 +491,8 @@ if __name__ == "__main__": print("Error: Argument 'setup' for Inno Setup is Windows only.") sys.exit(1) - if simPack: - simplePackage(hostOS) + if winPack: + makeWindowsPackage() if doFreeze: freezePackage(buildWindowed, oneFile, makeSetup, hostOS) diff --git a/setup/win_setup.iss b/setup/win_setup.iss index ba2c9990..a5456c1e 100644 --- a/setup/win_setup.iss +++ b/setup/win_setup.iss @@ -6,7 +6,7 @@ #define nwAppVersion "%%version%%" #define nwAppPublisher "novelWriter" #define nwAppURL "http://novelWriter.io" -#define nwAppExeName "novelWriter.exe" +#define nwAppExeName "novelWriter.pyw" [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. @@ -45,16 +45,15 @@ Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescrip Source: "{#nwAppDir}\novelWriter\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [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 +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}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent +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}\novelWriter.exe"" ""%1""" -Root: HKA; Subkey: "Software\Classes\Applications\novelWriter.exe\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: "" +Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\nw\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: "" From 2ea0703701ea0c951ad6f163b1f509a49d7d3856 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 21 Jan 2021 16:45:16 +0100 Subject: [PATCH 3/7] Preserve both methods for building Inno Setup installers --- make.py | 70 +++++++++++++++------- setup/win_setup_exe.iss | 59 ++++++++++++++++++ setup/{win_setup.iss => win_setup_pyz.iss} | 0 3 files changed, 106 insertions(+), 23 deletions(-) create mode 100644 setup/win_setup_exe.iss rename setup/{win_setup.iss => win_setup_pyz.iss} (100%) diff --git a/make.py b/make.py index a699ad13..55a909fd 100755 --- a/make.py +++ b/make.py @@ -39,7 +39,7 @@ def installPackages(hostOS): print("#######################") print("") - installQueue = ["pip", "pyinstaller", "-r requirements.txt"] + installQueue = ["pip", "-r requirements.txt"] if hostOS == OS_DARWIN: installQueue.append("pyobjc") @@ -336,7 +336,7 @@ def makeWindowsPackage(): # Inno Setup Builder # =============================================================================================== # -def innoSetup(): +def innoSetup(setupType): """Run the Inno Setup tool to build a setup.exe file for Windows. """ print("") @@ -346,7 +346,7 @@ def innoSetup(): # Read the iss template issData = "" - with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile: + with open(os.path.join("setup", "win_setup_%s.iss" % setupType), mode="r") as inFile: issData = inFile.read() import nw # noqa: E402 @@ -425,7 +425,8 @@ if __name__ == "__main__": # Flags and Variables buildWindowed = True oneFile = False - makeSetup = False + makeSetupExe = False + makeSetupPyz = False doFreeze = False winPack = False @@ -433,20 +434,30 @@ if __name__ == "__main__": "\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" + "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: @@ -482,11 +493,21 @@ if __name__ == "__main__": sys.argv.remove("winpack") winPack = True - if "setup" in sys.argv: - sys.argv.remove("setup") + if "setup_exe" in sys.argv: + sys.argv.remove("setup_exe") if hostOS == OS_WIN: oneFile = False - makeSetup = True + 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) @@ -495,9 +516,12 @@ if __name__ == "__main__": makeWindowsPackage() if doFreeze: - freezePackage(buildWindowed, oneFile, makeSetup, hostOS) + freezePackage(buildWindowed, oneFile, makeSetupExe, hostOS) - if makeSetup: - innoSetup() + if makeSetupExe: + innoSetup("exe") + + if makeSetupPyz: + innoSetup("pyz") # END Main diff --git a/setup/win_setup_exe.iss b/setup/win_setup_exe.iss new file mode 100644 index 00000000..bed7c5ed --- /dev/null +++ b/setup/win_setup_exe.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.exe" + +[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}\{#nwAppExeName}" +Name: "{autodesktop}\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"; Tasks: desktopicon + +[Run] +Filename: "{app}\{#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}\{#nwAppExeName}"" ""%1""" +Root: HKA; Subkey: "Software\Classes\Applications\{#nwAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: "" diff --git a/setup/win_setup.iss b/setup/win_setup_pyz.iss similarity index 100% rename from setup/win_setup.iss rename to setup/win_setup_pyz.iss From 7c2c4b26b300e069eb2beb5aa7099b2bda557f9a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Thu, 21 Jan 2021 17:24:54 +0100 Subject: [PATCH 4/7] Merge make.py into setup.py --- make.py | 527 -------------------------------------------- setup.py | 574 +++++++++++++++++++++++++++++++++++++++++++++--- setup/README.md | 70 +++--- 3 files changed, 581 insertions(+), 590 deletions(-) delete mode 100755 make.py 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. From 5f86ab27bb00774ea5e01870e4c5799129451da0 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Jan 2021 14:25:12 +0100 Subject: [PATCH 5/7] Complete the zipapp packaging for Windows --- nw/config.py | 14 +++++--- setup.py | 74 ++++++++++++++++++++++++----------------- setup/win_setup_pyz.iss | 4 +-- 3 files changed, 55 insertions(+), 37 deletions(-) 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 4c1e834f..d5a6aff0 100755 --- a/setup.py +++ b/setup.py @@ -216,12 +216,13 @@ def buildSampleZip(): # Make Simple Package (winpack) ## -def makeWindowsPackage(): +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 # ============= @@ -230,7 +231,10 @@ def makeWindowsPackage(): 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) @@ -240,23 +244,24 @@ def makeWindowsPackage(): # Download Python Embeddable # ========================== - print("") - print("# Downloading Python Embeddable") - print("# =============================") - print("") + if embedPython: + 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) + 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) + urllib.request.urlretrieve(pyUrl, pyZip) - print("Extracting ...") - with zipfile.ZipFile(pyZip, "r") as inFile: - inFile.extractall(outDir) + print("Extracting ...") + with zipfile.ZipFile(pyZip, "r") as inFile: + inFile.extractall(outDir) - os.unlink(pyZip) - print("") + os.unlink(pyZip) + print("") # Make sample.zip # =============== @@ -281,7 +286,7 @@ def makeWindowsPackage(): cpIgnore = shutil.ignore_patterns("__pycache__") print("Copying: nw") - shutil.copytree("nw", os.path.join(outDir, "nw"), ignore=cpIgnore) + 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)) @@ -289,8 +294,11 @@ def makeWindowsPackage(): 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: + nwDir = os.path.join(outDir, "nw") + 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" @@ -298,7 +306,9 @@ def makeWindowsPackage(): "import sys\n" "\n" "sys.path.insert(\n" - " 0, os.path.abspath(os.path.join(os.path.dirname(__file__), \"lib\"))\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" @@ -306,6 +316,9 @@ def makeWindowsPackage(): ) print("") + pyzFile = os.path.join(outDir, "novelWriter.pyz") + zipapp.create_archive(zipDir, target=pyzFile, interpreter="python3") + # Install Dependencies # ==================== @@ -722,9 +735,9 @@ if __name__ == "__main__": "\n" "Python Packaging:\n" "\n" - " winpack Creates a pyz package in a folder with all dependencies using the\n" + " pack-pyz 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" + " pack-exe 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" @@ -751,7 +764,8 @@ if __name__ == "__main__": makeSetupExe = False makeSetupPyz = False doFreeze = False - winPack = False + simplePack = False + embedPython = False # General # ======= @@ -783,16 +797,14 @@ if __name__ == "__main__": # Python Packaging # ================ - if "winpack" in sys.argv: - sys.argv.remove("winpack") + if "pack-pyz" in sys.argv: + sys.argv.remove("pack-pyz") + simplePack = True if hostOS == OS_WIN: - winPack = True - else: - print("Error: Command 'winpack' is Windows only.") - sys.exit(1) + embedPython = True - if "freeze" in sys.argv: - sys.argv.remove("freeze") + if "pack-exe" in sys.argv: + sys.argv.remove("pack-exe") doFreeze = True if "onefile" in sys.argv: @@ -843,8 +855,8 @@ if __name__ == "__main__": # For functions that are controlled by multiple flags, or need to be # run in a specific order. - if winPack: - makeWindowsPackage() + if simplePack: + makeSimplePackage(embedPython) if doFreeze: freezePackage(buildWindowed, oneFile, makeSetupExe, hostOS) diff --git a/setup/win_setup_pyz.iss b/setup/win_setup_pyz.iss index a5456c1e..f198bc22 100644 --- a/setup/win_setup_pyz.iss +++ b/setup/win_setup_pyz.iss @@ -6,7 +6,7 @@ #define nwAppVersion "%%version%%" #define nwAppPublisher "novelWriter" #define nwAppURL "http://novelWriter.io" -#define nwAppExeName "novelWriter.pyw" +#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. @@ -54,6 +54,6 @@ Filename: "{app}\pythonw.exe"; Parameters: "{#nwAppExeName}"; Description: "{cm [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}\nw\assets\icons\x-novelwriter-project.ico" +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: "" From a698cb6ef7f17f5fcc112e3d1c1eca6ac085f0bb Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Jan 2021 14:43:54 +0100 Subject: [PATCH 6/7] Automatically download the correct Python embed version --- setup.py | 29 ++++++++++++++++------------- setup/README.md | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/setup.py b/setup.py index d5a6aff0..8f405ef0 100755 --- a/setup.py +++ b/setup.py @@ -246,21 +246,23 @@ def makeSimplePackage(embedPython): if embedPython: print("") - print("# Downloading Python Embeddable") - print("# =============================") + print("# Adding 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) + 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) - os.unlink(pyZip) + print("Done") print("") # Make sample.zip @@ -294,7 +296,8 @@ def makeSimplePackage(embedPython): print("Copying: %s" % iconFile) shutil.copy2(os.path.join("setup", "icons", iconFile), os.path.join(outDir, iconFile)) - nwDir = os.path.join(outDir, "nw") + # 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") @@ -736,8 +739,8 @@ if __name__ == "__main__": "Python Packaging:\n" "\n" " pack-pyz Creates a pyz package in a folder with all dependencies using the\n" - " zipapp tool. This option is intended for Windows deployment.\n" - " pack-exe Freeze the package and produces a folder with all dependencies using\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" @@ -803,8 +806,8 @@ if __name__ == "__main__": if hostOS == OS_WIN: embedPython = True - if "pack-exe" in sys.argv: - sys.argv.remove("pack-exe") + if "freeze" in sys.argv: + sys.argv.remove("freeze") doFreeze = True if "onefile" in sys.argv: diff --git a/setup/README.md b/setup/README.md index cef06c2e..45aaf397 100644 --- a/setup/README.md +++ b/setup/README.md @@ -26,7 +26,7 @@ creating sample projects in the in-app New Project Wizard. ### Python Packaging -`winpack` – Creates a pyz package in a folder with all dependencies using the +`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 From 16c428dbd51134b8e8b01611d91111990f363fbc Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Jan 2021 14:51:27 +0100 Subject: [PATCH 7/7] Update docs to reflect that make.py has been merged into setup.py --- docs/source/int_started.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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