diff --git a/README.md b/README.md index 84a373a3..22cfd313 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,15 @@ You can also use the novelWriter [PPA](https://launchpad.net/~vkbo/+archive/ubun For more details, check the [Linux](https://novelwriter.readthedocs.io/en/latest/setup_linux.html) setup chapter in the documentation. -### Other Operating Systems +### Windows 10+ -For a regular installation, it is recommended that you download one of the minimal zip files from -the [Releases](https://github.com/vkbo/novelWriter/releases) page or the +The Release page has a `setup.exe` file for Windows 10, that should also work on other Windows +versions. The installer includes Python 3.10 and the library dependencies. + +### Other Install Options + +You can also download and install one of the minimal zip files from the +[Releases](https://github.com/vkbo/novelWriter/releases) page or the [novelwriter.io](https://novelwriter.io/) website. The [documentation](https://novelwriter.readthedocs.io/) has detailed install instructions for [Linux](https://novelwriter.readthedocs.io/en/latest/setup_linux.html), diff --git a/docs/source/setup_windows.rst b/docs/source/setup_windows.rst index 1bd01317..570d0e94 100644 --- a/docs/source/setup_windows.rst +++ b/docs/source/setup_windows.rst @@ -6,17 +6,25 @@ Setup on Windows This is a brief guide to how you can get novelWriter running on a Windows computer. -Unlike other operating systems, Windows does not come prepared with an environment to run Python -applications, so you must first install Python. After that, running novelWriter is straightforward. - - .. _a_setup_win_install: -Install novelWriter -=================== +Standard Installer +================== -The recommended way to run novelWriter on Windows is to download the "Minimal Package" option from -the `main website`_, or from the GitHub_ releases page (it's the same download file). +You can install novelWriter with dependencies embedded using the Windows Installer (setup.exe) file +from the `main website`_, or from the GitHub_ releases page. Installing it should be +straightforward. + +If you have any issues, try uninstalling the previous version and making a fresh install. + + +.. _a_setup_win_minimal: + +Minimal Package +=============== + +Alternatively, you can run novelWriter from the "Minimal Package" option from the `main website`_, +or from the GitHub_ releases page (it's the same download file). This is a zip file containing only the files you need to run novelWriter on Windows. In order to make it run on your system, you must first have Python installed (Step 1). Thereafter, a script @@ -119,14 +127,3 @@ The above steps can be reverted by running: pip uninstall -r requirements.txt .. _Python Package Index: https://pypi.org/ - - -Windows Installer ------------------ - -There used to be a Windows installer, but this is no longer provided. See the `installer issue`_ -for more info on why. You can still create the installer yourself if you want to. It can be -generated with the provided ``setup.py`` script. use the script's ``help`` command to get further -instructions. - -.. _installer issue: https://github.com/vkbo/novelWriter/issues/640 diff --git a/setup.py b/setup.py index 378378a4..42bb2edc 100755 --- a/setup.py +++ b/setup.py @@ -837,16 +837,34 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): ## -# Make Simple Package (build-pyz) +# Make Windows Setup EXE (build-win-exe) ## -def makeSimplePackage(embedPython): - """Run zipapp to freeze the packages. This assumes zipapp and pip - are already installed. +def makeWindowsEmbedded(sysArgs): + """Set up a package with embedded Python and dependencies for + Windows installation. """ import urllib.request import zipfile - import zipapp + import compileall + + print("") + print("Build Standalone Windows Package") + print("================================") + print("") + + minimalZip = None + packVersion = "none" + if len(sysArgs) >= 2: + if os.path.isfile(sysArgs[1]): + minimalZip = sysArgs[1] + + if minimalZip is None: + print("Please provide the path to the minimal win pacage as an argument") + sys.exit(1) + + packVersion = os.path.basename(minimalZip).split("-")[1] + print("Version: %s" % packVersion) # Set Up Folder # ============= @@ -855,100 +873,66 @@ def makeSimplePackage(embedPython): os.mkdir("dist") outDir = os.path.join("dist", "novelWriter") - zipDir = os.path.join("dist", "zipapp_temp") libDir = os.path.join(outDir, "lib") - if os.path.isdir(zipDir): - shutil.rmtree(zipDir) if os.path.isdir(outDir): shutil.rmtree(outDir) os.mkdir(outDir) os.mkdir(libDir) + # Extract Source Files + # ==================== + + print("Extracting source files ...") + with zipfile.ZipFile(minimalZip, "r") as inFile: + inFile.extractall(outDir) + + shutil.copyfile( + os.path.join(outDir, "novelwriter", "assets", "icons", "novelwriter.ico"), + os.path.join(outDir, "novelwriter.ico") + ) + + compileall.compile_dir(os.path.join(outDir, "novelwriter")) + + print("Done") + print("") + # Download Python Embeddable # ========================== - if embedPython: - print("") - print("# Adding Python Embeddable") - print("# ========================") - print("") + print("Adding Python embeddable ...") - 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) + pyVers = "%d.%d.%d" % (sys.version_info[:3]) + zipFile = "python-%s-embed-amd64.zip" % pyVers + pyZip = os.path.join("dist", zipFile) + if not os.path.isfile(pyZip): + pyUrl = f"https://www.python.org/ftp/python/{pyVers}/{zipFile}" + print("Downloading: %s" % pyUrl) + urllib.request.urlretrieve(pyUrl, pyZip) - print("Extracting ...") - with zipfile.ZipFile(pyZip, "r") as inFile: - inFile.extractall(outDir) + print("Extracting ...") + with zipfile.ZipFile(pyZip, "r") as inFile: + inFile.extractall(outDir) - print("Done") - print("") - - # Build Additional Assets - # ======================= - - buildQtI18n() - buildSampleZip() - buildPdfManual() - - # Copy Package Files - # ================== - - print("") - print("# Copying Package Files") - print("# =====================") + print("Done") print("") - copyList = ["CREDITS.md", "CHANGELOG.md", "LICENSE.md", "requirements.txt"] - iconList = ["novelwriter.ico", "x-novelwriter-project.ico"] - cpIgnore = shutil.ignore_patterns("__pycache__") + # Sort Out Licence Files + # ====================== - print("Copying: novelwriter") - shutil.copytree("novelwriter", os.path.join(zipDir, "novelwriter"), ignore=cpIgnore) - for copyFile in copyList: - print("Copying: %s" % copyFile) - shutil.copy2(copyFile, os.path.join(outDir, copyFile)) - for iconFile in iconList: - print("Copying: %s" % iconFile) - shutil.copy2(os.path.join("setup", "icons", iconFile), os.path.join(outDir, iconFile)) - - # Move assets to outDir as it should not be packed with the rest - print("Copying: assets") - os.rename(os.path.join(zipDir, "novelwriter", "assets"), os.path.join(outDir, "assets")) - - print("Writing: __main__.py") - writeFile(os.path.join(zipDir, "__main__.py"), ( - "#!/usr/bin/env python3\n" - "\n" - "import os\n" - "import sys\n" - "\n" - "sys.path.insert(\n" - " 0, os.path.abspath(\n" - " os.path.join(os.path.dirname(__file__), os.path.pardir, \"lib\")\n" - " )\n" - ")\n\n" - "if __name__ == \"__main__\":\n" - " import novelwriter\n" - " novelwriter.main()\n" - )) - print("") - - pyzFile = os.path.join(outDir, "novelWriter.pyz") - zipapp.create_archive(zipDir, target=pyzFile, interpreter="/usr/bin/env python3") + os.rename( + os.path.join(outDir, "LICENSE.txt"), + os.path.join(outDir, "PYTHON-LICENSE.txt") + ) + shutil.copyfile( + os.path.join("setup", "iss_license.txt"), + os.path.join(outDir, "LICENSES.txt") + ) # Install Dependencies # ==================== - print("") - print("# Installing Dependencies") - print("# =======================") - print("") + print("Install dependencies ...") sysCmd = [sys.executable] sysCmd += "-m pip install -r requirements.txt --target".split() @@ -960,54 +944,143 @@ def makeSimplePackage(embedPython): print(str(exc)) 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("Done") print("") - # Remove Unneeded Library Files - # ============================= + # Update Launch File + # ================== - 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) + print("Updating starting script ...") + + with open(os.path.join(outDir, "novelWriter.pyw"), mode="w") as outFile: + outFile.write( + "#!/usr/bin/env python3\n" + "import os\n" + "import sys\n" + "\n" + "os.curdir = os.path.abspath(os.path.dirname(__file__))\n" + "sys.path.insert(0, os.path.join(os.curdir, \"lib\"))\n" + "\n" + "if __name__ == \"__main__\":\n" + " import novelwriter\n" + " novelwriter.main(sys.argv[1:])\n" + ) + + print("Done") + print("") + + # Clean Up Files + # ============== + + def unlinkIfFound(delFile): if os.path.isfile(delFile): - print("Deleting: %s" % delFile) os.unlink(delFile) + print("Deleted: %s" % delFile) - qmlDir = os.path.join(libDir, "PyQt5", "Qt", "qml") - if os.path.isdir(qmlDir): - shutil.rmtree(qmlDir) + def deleteFolder(delPath): + if os.path.isdir(delPath): + shutil.rmtree(delPath) + print("Deleted: %s" % delPath) + + print("Deleting Redundant Files") + print("========================") + print("") + + print("Dictionaries") + dictDir = os.path.join(libDir, "enchant", "data", "mingw64", "share", "enchant", "hunspell") + for dictFile in os.listdir(dictDir): + dictPath = os.path.join(dictDir, dictFile) + if not os.path.isfile(dictPath): + continue + if dictFile.startswith("en_GB"): + continue + if dictFile.startswith("en_US"): + continue + unlinkIfFound(dictPath) + + print("Translations") + qmDir = os.path.join(libDir, "PyQt5", "Qt5", "translations") + for qmFile in os.listdir(qmDir): + qmPath = os.path.join(qmDir, qmFile) + if not os.path.isfile(qmPath): + continue + if qmFile.startswith("qtbase"): + continue + unlinkIfFound(qmPath) print("") - print("Done!") + + delQt5 = [ + "Qt5Bluetooth", "Qt5DBus", "Qt5Designer", "Qt5Designer", "Qt5Help", "Qt5Location", + "Qt5Multimedia", "Qt5MultimediaWidgets", "Qt5Network", "Qt5Nfc", "Qt5OpenGL", + "Qt5Positioning", "Qt5PositioningQuick", "Qt5Qml", "Qt5QmlModels", "Qt5QmlWorkerScript", + "Qt5Quick", "Qt5Quick3D", "Qt5Quick3DAssetImport", "Qt5Quick3DRender", + "Qt5Quick3DRuntimeRender", "Qt5Quick3DUtils", "Qt5QuickControls2", "Qt5QuickParticles", + "Qt5QuickShapes", "Qt5QuickTemplates2", "Qt5QuickTest", "Qt5QuickWidgets", "Qt5Sensors", + "Qt5SerialPort", "Qt5Sql", "Qt5Test", "Qt5TextToSpeech", "Qt5WebChannel", "Qt5WebSockets", + "Qt5WebView", "Qt5Xml", "Qt5XmlPatterns" + ] + pyQt5Dir = os.path.join(libDir, "PyQt5") + bindDir = os.path.join(pyQt5Dir, "bindings") + qt5Dir = os.path.join(pyQt5Dir, "Qt5") + binDir = os.path.join(qt5Dir, "bin") + plugDir = os.path.join(qt5Dir, "plugins") + + delFiles = [ + os.path.join(binDir, "opengl32sw.dll") + ] + delFolders = [ + os.path.join(qt5Dir, "qml"), + os.path.join(plugDir, "geoservices"), + os.path.join(plugDir, "playlistformats"), + os.path.join(plugDir, "renderers"), + os.path.join(plugDir, "sensorgestures"), + os.path.join(plugDir, "sensors"), + os.path.join(plugDir, "sqldrivers"), + os.path.join(plugDir, "texttospeech"), + os.path.join(plugDir, "webview"), + ] + + for qt5Item in delQt5: + print("Qt5 Component %s" % qt5Item) + qtItem = qt5Item.replace("Qt5", "Qt") + unlinkIfFound(os.path.join(binDir, qt5Item+".dll")) + unlinkIfFound(os.path.join(pyQt5Dir, qtItem+".pyd")) + unlinkIfFound(os.path.join(pyQt5Dir, qtItem+".pyi")) + deleteFolder(os.path.join(bindDir, qtItem)) + print("") + + print("Additional Files") + for delItem in delFiles: + unlinkIfFound(delItem) print("") + print("Additional Folders") + for delItem in delFolders: + deleteFolder(delItem) + print("") + + print("Done") + print("") + + print("Running Inno Setup") + print("##################") + print("") + + # Read the iss template + issData = readFile(os.path.join("setup", "win_setup_embed.iss")) + issData = issData.replace(r"%%version%%", packVersion) + issData = issData.replace(r"%%dir%%", os.getcwd()) + writeFile("setup.iss", issData) + print("") + + try: + subprocess.call(["iscc", "setup.iss"]) + except Exception as exc: + print("Inno Setup failed with error:") + print(str(exc)) + sys.exit(1) + return @@ -1439,40 +1512,6 @@ def winUninstall(): return -# =============================================================================================== # -# Windows Installers -# =============================================================================================== # - -## -# Inno Setup Builder (setup-pyz) -## - -def innoSetup(): - """Run the Inno Setup tool to build a setup.exe file for Windows based on the pyz package. - """ - print("") - print("Running Inno Setup") - print("##################") - print("") - - # Read the iss template - numVers, _, _ = extractVersion() - issData = readFile(os.path.join("setup", "win_setup_pyz.iss")) - issData = issData.replace(r"%%version%%", numVers) - issData = issData.replace(r"%%dir%%", os.getcwd()) - writeFile("setup.iss", issData) - print("") - - try: - subprocess.call(["iscc", "setup.iss"]) - except Exception as exc: - print("Inno Setup failed with error:") - print(str(exc)) - sys.exit(1) - - return - - # =============================================================================================== # # Process Command Line # =============================================================================================== # @@ -1565,11 +1604,8 @@ if __name__ == "__main__": " build-ubuntu Build a .deb packages Launchpad. Add --sign to ", " sign package. Add --first to set build number to 0.", " Add --snapshot to make a snapshot package.", - " build-pyz Build a .pyz package in a folder with all dependencies", - " using the zipapp tool. On Windows, python embeddable is", - " added to the folder.", - " setup-pyz Build a Windows executable installer from a zipapp package", - " using Inno Setup. Must run 'build-pyz' first.", + " build-win-exe Build a setup.exe file with Python embedded for Windows.", + " The package must be built from a minimal windows zip file.", "", "System Install:", "", @@ -1589,11 +1625,6 @@ if __name__ == "__main__": "", ] - # Flags and Variables - makeSetupPyz = False - simplePack = False - embedPython = False - # General # ======= @@ -1668,11 +1699,10 @@ if __name__ == "__main__": print("ERROR: Command 'build-ubuntu' can only be used on Linux") sys.exit(1) - if "build-pyz" in sys.argv: - sys.argv.remove("build-pyz") - simplePack = True - if hostOS == OS_WIN: - embedPython = True + if "build-win-exe" in sys.argv: + sys.argv.remove("build-win-exe") + makeWindowsEmbedded(sys.argv) + sys.exit(0) # Don't continue execution # General Installers # ================== @@ -1709,27 +1739,8 @@ if __name__ == "__main__": print("ERROR: Command 'win-uninstall' can only be used on Windows") sys.exit(1) - # Windows Setup Installer - # ======================= - - if "setup-pyz" in sys.argv: - sys.argv.remove("setup-pyz") - if hostOS == OS_WIN: - makeSetupPyz = True - else: - print("Error: Command 'setup-pyz' for Inno Setup is Windows only.") - sys.exit(1) - # Actions # ======= - # For functions that are controlled by multiple flags, or need to be - # run in a specific order. - - if simplePack: - makeSimplePackage(embedPython) - - if makeSetupPyz: - innoSetup() if len(sys.argv) <= 1: # Nothing more to do diff --git a/setup/iss_license.txt b/setup/iss_license.txt new file mode 100644 index 00000000..63223cfb --- /dev/null +++ b/setup/iss_license.txt @@ -0,0 +1,49 @@ +novelWriter License + +Copyright (C) 2018-2022 Veronica Berglyd Olsen +License: GPL v3+ + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +Dependencies + +Qt5 +Copyright: Qt Company +Website: +License: LGPL v3 + +PyQt5 / PyQt5-sip +Copyright: Riverbank Computing +Website: +License: GPL v3 + +lxml +Copyright: Infrae +Website: +License: BSD + +Enchant +Copyright: Dom Lachowicz +Website: +License: LGPL v2.1 + +PyEnchant +Copyright: Dimitri Merejkowsky +Website: +License: LGPL v2.1 + +Typicons +Copyright: Stephen Hutchings +Website: +License: CC BY-SA 4.0 diff --git a/setup/win_setup_pyz.iss b/setup/win_setup_embed.iss similarity index 79% rename from setup/win_setup_pyz.iss rename to setup/win_setup_embed.iss index 6fdb17ce..c28e15b8 100644 --- a/setup/win_setup_pyz.iss +++ b/setup/win_setup_embed.iss @@ -5,12 +5,10 @@ #define nwAppName "novelWriter" #define nwAppVersion "%%version%%" #define nwAppPublisher "novelWriter" -#define nwAppURL "http://novelWriter.io" -#define nwAppExeName "novelWriter.pyz" +#define nwAppURL "https://novelWriter.io" +#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. -; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) AppId={{459A75D0-951F-4932-9809-6002EC8E733E} AppName={#nwAppName} AppVersion={#nwAppVersion} @@ -19,15 +17,14 @@ AppPublisher={#nwAppPublisher} AppPublisherURL={#nwAppURL} AppSupportURL={#nwAppURL} AppUpdatesURL={#nwAppURL} +SetupIconFile=setup\icons\novelwriter.ico DefaultDirName={autopf}\{#nwAppName} +LicenseFile=setup\iss_license.txt 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-pyz-setup +OutputBaseFilename=novelwriter-{#nwAppVersion}-win10-amd64-standalone-setup Compression=lzma SolidCompression=yes WizardStyle=modern @@ -56,4 +53,4 @@ Root: HKA; Subkey: "Software\Classes\.nwx\OpenWithProgids"; ValueType: string; V Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx"; ValueType: string; ValueName: ""; ValueData: "novelWriter Project File"; Flags: uninsdeletekey Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\assets\icons\x-novelwriter-project.ico" Root: HKA; Subkey: "Software\Classes\novelWriterProject.nwx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\pythonw.exe"" ""{app}\{#nwAppExeName}"" ""%1""" -Root: HKA; Subkey: "Software\Classes\Applications\{#nwAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: "" +Root: HKA; Subkey: "Software\Classes\Applications\{#nwAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".nwx"; ValueData: ""; Flags: uninsdeletekey