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:
committed by
GitHub
parent
71419eeda3
commit
a0cdf44deb
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user