Add a windows setup builder (#981)

* Add a package builder for windows install
* Remove old win installer build
* Update setup instructions
* Some minor clarifications to docs and README
This commit is contained in:
Veronica Berglyd Olsen
2022-02-04 20:37:04 +01:00
committed by GitHub
parent 71419eeda3
commit a0cdf44deb
5 changed files with 275 additions and 216 deletions
+8 -3
View File
@@ -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),
+16 -19
View File
@@ -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
+196 -185
View File
@@ -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
+49
View File
@@ -0,0 +1,49 @@
novelWriter License
Copyright (C) 2018-2022 Veronica Berglyd Olsen
License: GPL v3+ <https://www.gnu.org/licenses/gpl-3.0.html>
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 <https://www.gnu.org/licenses/>.
Dependencies
Qt5
Copyright: Qt Company
Website: <https://www.qt.io/>
License: LGPL v3 <https://www.gnu.org/licenses/lgpl-3.0.html>
PyQt5 / PyQt5-sip
Copyright: Riverbank Computing
Website: <https://www.riverbankcomputing.com/software/pyqt/>
License: GPL v3 <https://www.gnu.org/licenses/gpl-3.0.html>
lxml
Copyright: Infrae
Website: <https://lxml.de/>
License: BSD
Enchant
Copyright: Dom Lachowicz
Website: <https://abiword.github.io/enchant/>
License: LGPL v2.1 <https://www.gnu.org/licenses/lgpl-2.1.html>
PyEnchant
Copyright: Dimitri Merejkowsky
Website: <https://pyenchant.github.io/pyenchant/>
License: LGPL v2.1 <https://www.gnu.org/licenses/lgpl-2.1.html>
Typicons
Copyright: Stephen Hutchings
Website: <https://www.s-ings.com/typicons/>
License: CC BY-SA 4.0 <http://creativecommons.org/licenses/by-sa/4.0/>
@@ -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