From 02312054fb461fe4931b4e92be1db1bc3879a2b4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Wed, 26 Jul 2023 19:28:34 +0200 Subject: [PATCH] Split up setup.py and move packaging tools to new script --- .github/workflows/build.yml | 4 +- .github/workflows/test_linux.yml | 2 +- MANIFEST.in | 2 +- docs/source/int_source.rst | 15 +- i18n/README.md | 4 +- pkgutils.py | 2035 +++++++++++++++++++++++++++++ setup.py | 2080 +----------------------------- setup/README.md | 28 - setup/macos/build.sh | 4 +- setup/make_clean.sh | 4 +- setup/make_pip.sh | 4 +- setup/make_release.sh | 16 +- setup/make_snapshot.sh | 8 +- setup/windows_install.bat | 14 +- setup/windows_uninstall.bat | 14 +- 15 files changed, 2088 insertions(+), 2146 deletions(-) create mode 100755 pkgutils.py delete mode 100644 setup/README.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 27a961e4..91d91a86 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: run: pip install -r docs/source/requirements.txt - name: Build Assets - run: python setup.py qtlrelease sample manual + run: python pkgutils.py qtlrelease sample manual - name: Upload Artifacts uses: actions/upload-artifact@v3 @@ -64,7 +64,7 @@ jobs: path: novelwriter/assets - name: Build AppImage - run: python setup.py build-appimage --linux-tag ${{ matrix.linux-tag }} --python-version ${{ matrix.python-version }} + run: python pkgutils.py build-appimage --linux-tag ${{ matrix.linux-tag }} --python-version ${{ matrix.python-version }} - name: Upload Artifacts uses: actions/upload-artifact@v3 diff --git a/.github/workflows/test_linux.yml b/.github/workflows/test_linux.yml index 6a778d2f..0ec531c0 100644 --- a/.github/workflows/test_linux.yml +++ b/.github/workflows/test_linux.yml @@ -32,7 +32,7 @@ jobs: run: | pip install -U -r requirements.txt -r requirements-dev.txt - name: Run Build Commands - run: python setup.py qtlrelease sample + run: python pkgutils.py qtlrelease sample - name: Run Tests run: | export QT_QPA_PLATFORM=offscreen diff --git a/MANIFEST.in b/MANIFEST.in index 0c89f7ca..5de01190 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include setup.py +include pkgutils.py include LICENSE.md include CREDITS.md include CHANGELOG.md diff --git a/docs/source/int_source.rst b/docs/source/int_source.rst index 3630b537..e9ae6cec 100644 --- a/docs/source/int_source.rst +++ b/docs/source/int_source.rst @@ -18,21 +18,18 @@ to build the various components like the translation files and documentation. ``python3`` may be needed instead. Likewise, ``pip`` may need to be replaced with ``pip3``. Most of the custom commands for building packages of novelWriter, or building assets, are contained -in the ``setup.py`` script in the root of the source code. You can list the available commands by -running: +in the ``pkgutils.py`` script in the root of the source code. You can list the available commands +by running: .. code-block:: bash - python setup.py help + python pkgutils.py help .. warning:: Calling ``setup.py install`` has been deprecated for a while, and this approach is no longer actively supported in novelWriter either. - In a future version, the packaging and asset build tools part of the current ``setup.py`` file - will be moved to another script and the ``setup.py`` file removed entirely. - .. _a_source_depend: @@ -116,7 +113,7 @@ You can build the ``.qm`` files with: .. code-block:: bash - python setup.py qtlrelease + python pkgutils.py qtlrelease This requires that the Qt Linguist tool is installed on your system. On Ubuntu and Debian, the needed package is called ``qttools5-dev-tools``. @@ -137,7 +134,7 @@ the ``assets`` folder of the source. This file can be built from setup script by .. code-block:: bash - python setup.py sample + python pkgutils.py sample .. _a_source_docs: @@ -165,7 +162,7 @@ You can also build a PDF manual from the documentation using the setup script: .. code-block:: bash - python setup.py manual + python pkgutils.py manual This will build the documentation as a PDF using LaTeX. The file will then be copied into the assets folder and made available in the :guilabel:`Help` menu in novelWriter. The Sphinx build diff --git a/i18n/README.md b/i18n/README.md index b2dd8cc9..159eee73 100644 --- a/i18n/README.md +++ b/i18n/README.md @@ -76,7 +76,7 @@ The `i18n` folder at the root of the repository contains the `nw_XX.ts` translat Whether to add a new language to the translation framework, or to update the file against the current source code, you must first run the `qtlupdate` command: ```bash -python3 setup.py qtlupdate i18n/nw_XX.ts +python3 pkgutils.py qtlupdate i18n/nw_XX.ts ``` The file name must be in the correct format. All translation files must be located in the `i18n` @@ -101,7 +101,7 @@ The application does not use `.ts` files directly. The `novelwriter/assets/i18n/ the actual files used to translate the GUI into another language other than the default British English. These files are not generated by default, but they can be built with: ```bash -python3 setup.py qtlrelease +python3 pkgutils.py qtlrelease ``` You can now test the translation in novelWriter. The Preferences dialog should list the newly added diff --git a/pkgutils.py b/pkgutils.py new file mode 100755 index 00000000..a5570d9c --- /dev/null +++ b/pkgutils.py @@ -0,0 +1,2035 @@ +#!/usr/bin/env python3 +""" +novelWriter – Packaging Utils +============================= + +File History: +Created: 2019-05-16 [0.5.1] +Renamed: 2023-07-26 [2.1b1] + +This file is a part of novelWriter +Copyright 2018–2023, Veronica Berglyd Olsen + +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 . +""" + +import os +import sys +import shutil +import zipfile +import datetime +import subprocess +import email.utils + +OS_NONE = 0 +OS_LINUX = 1 +OS_WIN = 2 +OS_DARWIN = 3 + + +# =============================================================================================== # +# Utilities +# =============================================================================================== # + +def extractVersion(beQuiet=False): + """Extract the novelWriter version number without having to import + anything else from the main package. + """ + def getValue(theString): + theBits = theString.partition("=") + return theBits[2].strip().strip('"') + + numVers = "Unknown" + hexVers = "Unknown" + relDate = "Unknown" + initFile = os.path.join("novelwriter", "__init__.py") + try: + with open(initFile, mode="r", encoding="utf-8") as inFile: + for aLine in inFile: + if aLine.startswith("__version__"): + numVers = getValue((aLine)) + if aLine.startswith("__hexversion__"): + hexVers = getValue((aLine)) + if aLine.startswith("__date__"): + relDate = getValue((aLine)) + except Exception as exc: + print("Could not read file: %s" % initFile) + print(str(exc)) + + if not beQuiet: + print("novelWriter version: %s (%s) at %s" % (numVers, hexVers, relDate)) + + return numVers, hexVers, relDate + + +def compactVersion(version): + """Make the version number more compact.""" + return version.replace("-alpha", "a").replace("-beta", "b").replace("-rc", "rc") + + +def sysCall(callArgs, cwd=None): + """Wrapper function for system calls.""" + sysP = subprocess.Popen( + callArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + shell=True, cwd=cwd + ) + stdOut, stdErr = sysP.communicate() + return stdOut.decode("utf-8"), stdErr.decode("utf-8"), sysP.returncode + + +def readFile(fileName): + """Read an entire file and return as a string.""" + with open(fileName, mode="r", encoding="utf-8") as inFile: + return inFile.read() + + +def writeFile(fileName, writeText): + """Write string to file.""" + with open(fileName, mode="w+", encoding="utf-8") as outFile: + outFile.write(writeText) + + +def toUpload(srcPath, dstName=None): + """Copy a file produced by one of the build functions to the upload + directory. The file can optionally be given a new name. + """ + uplDir = "dist_upload" + if not os.path.isdir(uplDir): + os.mkdir(uplDir) + if dstName is None: + dstName = os.path.basename(srcPath) + shutil.copyfile(srcPath, os.path.join(uplDir, dstName)) + return + + +def makeCheckSum(sumFile, cwd=None): + """Create a SHA256 checksum file.""" + try: + if cwd is None: + shaFile = sumFile+".sha256" + else: + shaFile = os.path.join(cwd, sumFile+".sha256") + with open(shaFile, mode="w") as fOut: + subprocess.call(["shasum", "-a", "256", sumFile], stdout=fOut, cwd=cwd) + print("SHA256 Sum: %s" % shaFile) + except Exception as exc: + print("Could not generate sha256 file") + print(str(exc)) + return "" + + return shaFile + + +# =============================================================================================== # +# 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") + elif hostOS == OS_WIN: + installQueue.append("pywin32") + + 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 exc: + print("Failed with error:") + print(str(exc)) + sys.exit(1) + + return + + +## +# Clean Build and Dist Folders (build-clean) +## + +def cleanBuildDirs(): + """Recursively delete the 'build' and 'dist' folders.""" + print("") + print("Cleaning up build environment ...") + print("") + + def removeFolder(rmDir): + if os.path.isdir(rmDir): + try: + shutil.rmtree(rmDir) + print("Deleted: %s" % rmDir) + except OSError: + print("Failed: %s" % rmDir) + else: + print("Missing: %s" % rmDir) + + removeFolder("build") + removeFolder("dist") + removeFolder("dist_deb") + removeFolder("dist_minimal") + removeFolder("dist_appimage") + removeFolder("novelWriter.egg-info") + + print("") + + return + + +# =============================================================================================== # +# Additional Builds +# =============================================================================================== # + +## +# Build PDF Manual (manual) +## + +def buildPdfManual(): + """This function will build the documentation as manual.pdf.""" + print("") + print("Building PDF Manual") + print("===================") + print("") + + buildFile = os.path.join("docs", "build", "latex", "manual.pdf") + finalFile = os.path.join("novelwriter", "assets", "manual.pdf") + + if os.path.isfile(finalFile): + # Make sure a new file is always generated + os.unlink(finalFile) + + try: + subprocess.call(["make", "clean"], cwd="docs") + exCode = subprocess.call(["make", "latexpdf"], cwd="docs") + if exCode == 0: + if os.path.isfile(finalFile): + os.unlink(finalFile) + print("") + os.rename(buildFile, finalFile) + else: + raise Exception(f"Build returned error code {exCode}") + + print("PDF manual build: OK") + print("") + + except Exception as exc: + print("PDF manual build: FAILED") + print("") + print(str(exc)) + print("") + print("Dependencies:") + print(" * pip install sphinx") + print(" * Package latexmk") + print(" * LaTeX build system") + print("") + print(" On Debian/Ubuntu, install: python3-sphinx latexmk texlive texlive-latex-extra") + print("") + sys.exit(1) + + if not os.path.isfile(finalFile): + print("No output file was found!") + print("") + sys.exit(1) + + return + + +## +# Qt Linguist QM Builder (qtlrelease) +## + +def buildQtI18n(): + """Build the lang.qm files for Qt Linguist.""" + print("") + print("Building Qt Localisation Files") + print("==============================") + + print("") + print("TS Files to Build:") + print("") + + tsList = [] + for aFile in os.listdir("i18n"): + aPath = os.path.join("i18n", aFile) + if os.path.isfile(aPath) and aFile.endswith(".ts") and aFile != "nw_base.ts": + tsList.append(aPath) + print(aPath) + + print("") + print("Building Translation Files:") + print("") + + try: + subprocess.call(["lrelease", "-verbose", *tsList]) + except Exception as exc: + print("Qt5 Linguist tools seem to be missing") + print("On Debian/Ubuntu, install: qttools5-dev-tools pyqt5-dev-tools") + print(str(exc)) + sys.exit(1) + + print("") + print("Moving QM Files to Assets") + print("") + + langDir = os.path.join("novelwriter", "assets", "i18n") + for langFile in os.listdir("i18n"): + langPath = os.path.join("i18n", langFile) + if not os.path.isfile(langPath): + continue + + if langFile.endswith(".qm"): + destPath = os.path.join(langDir, langFile) + os.rename(langPath, destPath) + print("Moved: %s -> %s" % (langPath, destPath)) + + print("") + + return + + +## +# Qt Linguist TS Builder (qtlupdate) +## + +def buildQtI18nTS(sysArgs): + """Build the lang.ts files for Qt Linguist.""" + print("") + print("Building Qt Translation Files") + print("=============================") + + try: + from PyQt6.lupdate import lupdate + except ImportError: + print("ERROR: This command requires lupdate from PyQt6") + sys.exit(1) + + print("") + print("Scanning Source Tree:") + print("") + + srcList = [os.path.join("i18n", "qtbase.py")] + for nRoot, _, nFiles in os.walk("novelwriter"): + if os.path.isdir(nRoot): + for aFile in nFiles: + aPath = os.path.join(nRoot, aFile) + if os.path.isfile(aPath) and aFile.endswith(".py"): + srcList.append(aPath) + + for aSource in srcList: + print(aSource) + + print("") + print("TS Files to Update:") + print("") + + tsList = [] + if len(sysArgs) >= 2: + for anArg in sysArgs[1:]: + if not (anArg.startswith("i18n") and anArg.endswith(".ts")): + continue + + fName = os.path.basename(anArg) + if not fName.startswith("nw_") and len(fName) > 6: + print("Skipping non-novelWriter TS file %s" % fName) + continue + + if os.path.isfile(anArg): + tsList.append(anArg) + elif os.path.exists(anArg): + pass + else: # Create an empty new language file + lCode = fName[3:-3] + writeFile(anArg, ( + "\n" + "\n" + f"\n" + )) + tsList.append(anArg) + + else: + print("No translation files selected for update ...") + print("") + return + + for aTS in tsList: + print(aTS) + + print("") + print("Updating Language Files:") + print("") + + # Using the pylupdate tool from PyQt6 as it supports TS file format 2.1. + lupdate(srcList, tsList, no_obsolete=True, no_summary=False) + + print("") + + return + + +## +# Generage MacOS PList +## + +def genMacOSPlist(): + """Set necessary values for .plist file for MacOS build.""" + numVers, _, _ = extractVersion() + pkgVers = compactVersion(numVers) + outDir = "setup/macos" + + copyrightYear = datetime.datetime.now().year + + # These keys are no longer used but are present for compatability + pkgVersMaj, pkgVersMin, _ = pkgVers.split(".") + + plistXML = readFile(f"{outDir}/Info.plist.template").format( + macosBundleSVers=pkgVers, + macosBundleVers=numVers, + macosBundleVersMajor=pkgVersMaj, + macosBundleVersMinor=pkgVersMin, + macosBundleCopyright=f"Copyright 2018–{copyrightYear}, Veronica Berglyd Olsen", + ) + + print(f"Writing Info.plist to {outDir}/Info.plist") + writeFile(f"{outDir}/Info.plist", plistXML) + + return + + +## +# Sample Project ZIP File Builder (sample) +## + +def buildSampleZip(): + """Bundle the sample project into a single zip file to be saved into + the novelwriter/assets folder for further bundling into builds. + """ + print("") + print("Building Sample ZIP File") + print("========================") + print("") + + srcSample = "sample" + dstSample = os.path.join("novelwriter", "assets", "sample.zip") + + if os.path.isdir(srcSample): + if os.path.isfile(dstSample): + os.unlink(dstSample) + + from zipfile import ZipFile + + with ZipFile(dstSample, "w") as zipObj: + print("Compressing: nwProject.nwx") + zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") + for docFile in os.listdir(os.path.join(srcSample, "content")): + print("Compressing: content/%s" % docFile) + srcDoc = os.path.join(srcSample, "content", docFile) + zipObj.write(srcDoc, "content/"+docFile) + + else: + print("Error: Could not find sample project source directory.") + sys.exit(1) + + print("") + print("Built file: %s" % dstSample) + print("") + + return + + +def cleanBuiltAssets(): + """Remove assets built by this script.""" + print("") + print("Removing Built Assets") + print("=====================") + print("") + + sampleZip = os.path.join("novelwriter", "assets", "sample.zip") + if os.path.isfile(sampleZip): + print(f"Deleted: {sampleZip}") + os.unlink(sampleZip) + + pdfManual = os.path.join("novelwriter", "assets", "manual.pdf") + if os.path.isfile(pdfManual): + print(f"Deleted: {pdfManual}") + os.unlink(pdfManual) + + i18nAssets = os.path.join("novelwriter", "assets", "i18n") + for i18nItem in os.listdir(i18nAssets): + i18nPath = os.path.join(i18nAssets, i18nItem) + if os.path.isfile(i18nPath) and i18nPath.endswith(".qm"): + print(f"Deleted: {i18nPath}") + os.unlink(i18nPath) + + print("") + + return + + +def checkAssetsExist(): + """Check that the necessary compiled assets exist ahead of a build. + """ + hasSample = False + hasManual = False + hasQmData = False + + sampleZip = os.path.join("novelwriter", "assets", "sample.zip") + if os.path.isfile(sampleZip): + print(f"Found: {sampleZip}") + hasSample = True + + pdfManual = os.path.join("novelwriter", "assets", "manual.pdf") + if os.path.isfile(pdfManual): + print(f"Found: {pdfManual}") + hasManual = True + + i18nAssets = os.path.join("novelwriter", "assets", "i18n") + for i18nItem in os.listdir(i18nAssets): + i18nPath = os.path.join(i18nAssets, i18nItem) + if os.path.isfile(i18nPath) and i18nPath.endswith(".qm"): + print(f"Found: {i18nPath}") + hasQmData = True + + return hasSample and hasManual and hasQmData + + +# =============================================================================================== # +# Python Packaging +# =============================================================================================== # + +## +# Import Translations (import-i18n) +## + +def importI18nUpdates(sysArgs): + """Import new translation files from a zip file.""" + print("") + print("Import Updated Translations") + print("===========================") + print("") + + fileName = None + if len(sysArgs) >= 2: + if os.path.isfile(sysArgs[1]): + fileName = sysArgs[1] + + if fileName is None: + print("File not found ...") + sys.exit(1) + + projPath = os.path.join("novelwriter", "assets", "i18n") + mainPath = "i18n" + + print("Loading file: %s" % fileName) + with zipfile.ZipFile(fileName) as zipObj: + for archFile in zipObj.namelist(): + if archFile.startswith("nw_") and archFile.endswith(".ts"): + zipObj.extract(archFile, mainPath) + print("Extracted: %s > %s" % (archFile, os.path.join(mainPath, archFile))) + elif archFile.startswith("project_") and archFile.endswith(".json"): + zipObj.extract(archFile, projPath) + print("Extracted: %s > %s" % (archFile, os.path.join(projPath, archFile))) + else: + print("Skipped: %s" % archFile) + + print("") + + return + + +## +# Make Minimal Package (minimal-zip) +## + +def makeMinimalPackage(targetOS): + """Pack the core source file in a single zip file.""" + from zipfile import ZipFile, ZIP_DEFLATED + + print("") + print("Building Minimal ZIP File") + print("=========================") + + bldDir = "dist_minimal" + if not os.path.isdir(bldDir): + os.mkdir(bldDir) + + if targetOS == OS_LINUX: + targName = "-linux" + print("Target OS: Linux") + elif targetOS == OS_DARWIN: + targName = "-darwin" + print("Target OS: Darwin") + elif targetOS == OS_WIN: + targName = "-win" + print("Target OS: Windows") + else: + targName = "" + print("") + + # Check Additional Assets + # ======================= + + if not checkAssetsExist(): + print("ERROR: Missing build assets") + sys.exit(1) + + # Build Minimal Zip + # ================= + + numVers, _, _ = extractVersion() + pkgVers = compactVersion(numVers) + zipFile = f"novelwriter-{pkgVers}-minimal{targName}.zip" + outFile = os.path.join(bldDir, zipFile) + if os.path.isfile(outFile): + os.unlink(outFile) + print("") + + rootFiles = [ + "README.md", + "LICENSE.md", + "CREDITS.md", + "CHANGELOG.md", + "requirements.txt", + "pkgutils.py", + "setup.cfg", + "pyproject.toml", + ] + + with ZipFile(outFile, "w", compression=ZIP_DEFLATED, compresslevel=9) as zipObj: + + for nRoot, _, nFiles in os.walk("novelwriter"): + if nRoot.endswith("__pycache__"): + print("Skipped: %s" % nRoot) + continue + + print("Added: %s/* [Files: %d]" % (nRoot, len(nFiles))) + for aFile in nFiles: + if aFile.endswith(".pyc"): + print("Skipping File: %s" % aFile) + continue + zipObj.write(os.path.join(nRoot, aFile)) + + if targetOS == OS_WIN: + zipObj.write("novelWriter.py", "novelWriter.pyw") + print("Added: novelWriter.pyw") + zipObj.write(os.path.join("setup", "windows_install.bat"), "windows_install.bat") + print("Added: windows_install.bat") + zipObj.write(os.path.join("setup", "windows_uninstall.bat"), "windows_uninstall.bat") + print("Added: windows_uninstall.bat") + + else: # Linux and Mac + # Add icons + for nRoot, _, nFiles in os.walk(os.path.join("setup", "data")): + print("Added: %s/* [Files: %d]" % (nRoot, len(nFiles))) + for aFile in nFiles: + zipObj.write(os.path.join(nRoot, aFile)) + + zipObj.write(os.path.join("setup", "description_pypi.md")) + print("Added: setup/description_pypi.md") + + zipObj.write("novelWriter.py") + print("Added: novelWriter.py") + + for aFile in rootFiles: + print("Added: %s" % aFile) + zipObj.write(aFile) + + zipObj.write(os.path.join("novelwriter", "assets", "manual.pdf"), "UserManual.pdf") + print("Added: UserManual.pdf") + + print("") + print("Created File: %s" % outFile) + + # Create Checksum File + # ==================== + + shaFile = makeCheckSum(zipFile, cwd=bldDir) + + toUpload(outFile) + toUpload(shaFile) + + print("") + + return + + +## +# Make Debian Package (build-deb) +## + +def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buildName=""): + """Build a Debian package.""" + print("") + print("Build Debian Package") + print("====================") + print("On Debian/Ubuntu install: dh-python python3-all debhelper devscripts") + print("") + + # Version Info + # ============ + + numVers, hexVers, relDate = extractVersion() + pkgVers = compactVersion(numVers) + relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") + pkgDate = email.utils.format_datetime(relDate.replace(hour=12, tzinfo=None)) + print("") + + if buildName: + pkgVers = f"{pkgVers}{buildName}" + + # Set Up Folder + # ============= + + bldDir = "dist_deb" + bldPkg = f"novelwriter_{pkgVers}" + outDir = f"{bldDir}/{bldPkg}" + debDir = f"{outDir}/debian" + datDir = f"{outDir}/data" + + if not os.path.isdir(bldDir): + os.mkdir(bldDir) + + if os.path.isdir(outDir): + print("Removing old build files ...") + print("") + shutil.rmtree(outDir) + + os.mkdir(outDir) + + # Check Additional Assets + # ======================= + + if not checkAssetsExist(): + print("ERROR: Missing build assets") + sys.exit(1) + + # Copy novelWriter Source + # ======================= + + print("Copying novelWriter source ...") + print("") + + for nPath, _, nFiles in os.walk("novelwriter"): + if nPath.endswith("__pycache__"): + print("Skipped: %s" % nPath) + continue + + pPath = f"{outDir}/{nPath}" + if not os.path.isdir(pPath): + os.mkdir(pPath) + + fCount = 0 + for fFile in nFiles: + nFile = f"{nPath}/{fFile}" + pFile = f"{pPath}/{fFile}" + + if fFile.endswith(".pyc"): + print("Skipped: %s" % nFile) + continue + + shutil.copyfile(nFile, pFile) + fCount += 1 + + print("Copied: %s/* [Files: %d]" % (nPath, fCount)) + + print("") + print("Copying or generating additional files ...") + print("") + + # Copy/Write Root Files + # ===================== + + copyFiles = ["LICENSE.md", "CREDITS.md", "CHANGELOG.md", "pyproject.toml"] + for copyFile in copyFiles: + shutil.copyfile(copyFile, f"{outDir}/{copyFile}") + print("Copied: %s" % copyFile) + + writeFile(f"{outDir}/MANIFEST.in", ( + "include LICENSE.md\n" + "include CREDITS.md\n" + "include CHANGELOG.md\n" + "include data/*\n" + "recursive-include novelwriter/assets *\n" + )) + print("Wrote: MANIFEST.in") + + writeFile(f"{outDir}/pkgutils.py", ( + "import setuptools\n" + "setuptools.setup()\n" + )) + print("Wrote: pkgutils.py") + + setupCfg = readFile("setup.cfg").replace( + "file: setup/description_pypi.md", "file: data/description_short.txt" + ) + writeFile(f"{outDir}/setup.cfg", setupCfg) + print("Wrote: setup.cfg") + + # Copy/Write Debian Files + # ======================= + + shutil.copytree("setup/debian", debDir) + print("Copied: debian/*") + + writeFile(f"{debDir}/changelog", ( + f"novelwriter ({pkgVers}) {distName}; urgency=low\n\n" + f" * Update to version {pkgVers}\n\n" + f" -- Veronica Berglyd Olsen {pkgDate}\n" + )) + print("Wrote: debian/changelog") + + # Copy/Write Data Files + # ===================== + + shutil.copytree("setup/data", datDir) + print("Copied: data/*") + + shutil.copyfile("setup/description_short.txt", f"{outDir}/data/description_short.txt") + print("Copied: data/description_short.txt") + + # Build Package + # ============= + + print("") + print("Running dpkg-buildpackage ...") + print("") + + if signKey is None: + signArgs = ["-us", "-uc"] + else: + signArgs = [f"-k{signKey}"] + + if sourceBuild: + subprocess.call(["debuild", "-S"] + signArgs, cwd=outDir) + toUpload(f"{bldDir}/{bldPkg}.tar.xz") + else: + subprocess.call(["dpkg-buildpackage"] + signArgs, cwd=outDir) + shutil.copyfile(f"{bldDir}/{bldPkg}.tar.xz", f"{bldDir}/{bldPkg}.debian.tar.xz") + toUpload(f"{bldDir}/{bldPkg}.debian.tar.xz") + toUpload(f"{bldDir}/{bldPkg}_all.deb") + + toUpload(makeCheckSum(f"{bldPkg}.debian.tar.xz", cwd=bldDir)) + toUpload(makeCheckSum(f"{bldPkg}_all.deb", cwd=bldDir)) + + print("") + print("Done!") + print("") + + if sourceBuild: + if hexVers[-2] == "f": + ppaName = "novelwriter" + else: + ppaName = "novelwriter-pre" + + return f"dput {ppaName}/{distName} {bldDir}/{bldPkg}_source.changes" + + return "" + + +## +# Make Launchpad Package (build-ubuntu) +## + +def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): + """Wrapper for building Debian packages for Launchpad.""" + print("") + print("Launchpad Packages") + print("==================") + print("") + + if isFirst or isSnapshot: + bldNum = "0" + else: + bldNum = input("Build number [0]: ") + if bldNum == "": + bldNum = "0" + + distLoop = [ + ("20.04", "focal"), + ("22.04", "jammy"), + ("22.10", "kinetic"), + ("23.04", "lunar"), + ] + + tStamp = datetime.datetime.now().strftime("%Y%m%d~%H%M%S") + if isSnapshot: + print(f"Building Ununtu SNAPSHOT~{tStamp} for:") + print("") + else: + print("Building Ubuntu packages for:") + print("") + for distNum, codeName in distLoop: + print(f" * Ubuntu {distNum} {codeName.title()}") + print("") + + if doSign: + signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" + else: + signKey = None + + print(f"Sign Key: {str(signKey)}") + print("") + + dputCmd = [] + for distNum, codeName in distLoop: + if isSnapshot: + buildName = f"+SNAPSHOT~{tStamp}~ubuntu{distNum}.0" + else: + buildName = f"~ubuntu{distNum}.{bldNum}" + + dCmd = makeDebianPackage( + signKey=signKey, + sourceBuild=True, + distName=codeName, + buildName=buildName + ) + dputCmd.append(dCmd) + + print("Packages Built") + print("==============") + print("") + for dCmd in dputCmd: + print(f" > {dCmd}") + print("") + + return + + +## +# Make AppImage (build-appimage) +## + +def makeAppImage(sysArgs): + """Build an AppImage.""" + import glob + import argparse + + try: + import python_appimage # noqa F401 + except ImportError: + print( + "ERROR: Package 'python-appimage' is missing on this system.\n" + " Please run 'pip install --user python-appimage' to install it.\n" + ) + sys.exit(1) + + print("") + print("Build AppImage") + print("==============") + print("") + + parser = argparse.ArgumentParser( + prog="build_appimage", + description="Build an AppImage", + epilog="see https://appimage.org/ for more details", + ) + parser.add_argument( + "--linux-tag", + nargs="?", + default="manylinux_2_28_x86_64", + help=( + "Linux compatibility tag (e.g. manylinux_2_28_x86_64)\n" + "see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n" + "and https://github.com/pypa/manylinux for a list of valid tags" + ), + ) + parser.add_argument( + "--python-version", nargs="?", default="3.11", help="Python version (e.g. 3.11)" + ) + + args, unparsedArgs = parser.parse_known_args(sysArgs) + + linuxTag = args.linux_tag + pythonVer = args.python_version + + # Version Info + # ============ + + numVers, _, relDate = extractVersion() + pkgVers = compactVersion(numVers) + relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") + print("") + + # Set Up Folder + # ============= + + bldDir = "dist_appimage" + bldPkg = f"novelwriter_{pkgVers}" + outDir = f"{bldDir}/{bldPkg}" + imageDir = f"{bldDir}/appimage" + + # Set Up Folders + # ============== + + if not os.path.isdir(bldDir): + os.mkdir(bldDir) + + if os.path.isdir(outDir): + print("Removing old build files ...") + print("") + shutil.rmtree(outDir) + + os.mkdir(outDir) + + if os.path.isdir(imageDir): + print("Removing old build metadata files ...") + print("") + shutil.rmtree(imageDir) + + os.mkdir(imageDir) + + # Remove old AppImages + outFiles = glob.glob(f"{bldDir}/*.AppImage") + if outFiles: + print("Removing old AppImages") + print("") + for image in outFiles: + try: + os.remove(image) + except OSError: + print("Error while deleting file : ", image) + + # Copy novelWriter Source + # ======================= + + print("Copying novelWriter source ...") + print("") + + for nPath, _, nFiles in os.walk("novelwriter"): + if nPath.endswith("__pycache__"): + print("Skipped: %s" % nPath) + continue + + pPath = f"{outDir}/{nPath}" + if not os.path.isdir(pPath): + os.mkdir(pPath) + + fCount = 0 + for fFile in nFiles: + nFile = f"{nPath}/{fFile}" + pFile = f"{pPath}/{fFile}" + + if fFile.endswith(".pyc"): + print("Skipped: %s" % nFile) + continue + + shutil.copyfile(nFile, pFile) + fCount += 1 + + print("Copied: %s/* [Files: %d]" % (nPath, fCount)) + + print("") + print("Copying or generating additional files ...") + print("") + + # Copy/Write Root Files + # ===================== + + copyFiles = ["LICENSE.md", "CREDITS.md", "CHANGELOG.md", "pyproject.toml"] + for copyFile in copyFiles: + shutil.copyfile(copyFile, f"{outDir}/{copyFile}") + print("Copied: %s" % copyFile) + + writeFile(f"{outDir}/MANIFEST.in", ( + "include LICENSE.md\n" + "include CREDITS.md\n" + "include CHANGELOG.md\n" + "include data/*\n" + "recursive-include novelwriter/assets *\n" + )) + print("Wrote: MANIFEST.in") + + writeFile(f"{outDir}/pkgutils.py", ( + "import setuptools\n" + "setuptools.setup()\n" + )) + print("Wrote: pkgutils.py") + + setupCfg = readFile("setup.cfg").replace( + "file: setup/description_pypi.md", "file: data/description_short.txt" + ) + writeFile(f"{outDir}/setup.cfg", setupCfg) + print("Wrote: setup.cfg") + + # Write Metadata + # ============== + + appDescription = readFile("setup/description_short.txt") + appdataXML = readFile("setup/novelwriter.appdata.xml").format(description=appDescription) + writeFile(f"{imageDir}/novelwriter.appdata.xml", appdataXML) + print("Wrote: novelwriter.appdata.xml") + + writeFile(f"{imageDir}/entrypoint.sh", ( + '#! /bin/bash \n' + '{{ python-executable }} -sE ${APPDIR}/opt/python{{ python-version }}/bin/novelwriter "$@"' + )) + print("Wrote: entrypoint.sh") + + writeFile(f"{imageDir}/requirements.txt", os.path.abspath(outDir)) + print("Wrote: requirements.txt") + + shutil.copyfile("setup/data/novelwriter.desktop", f"{imageDir}/novelwriter.desktop") + print("Copied: novelwriter.desktop") + + shutil.copyfile("setup/icons/novelwriter.svg", f"{imageDir}/novelwriter.svg") + print("Copied: novelwriter.svg") + + shutil.copyfile( + "setup/data/hicolor/256x256/apps/novelwriter.png", f"{imageDir}/novelwriter.png" + ) + print("Copied: novelwriter.png") + + # Build AppImage + # ============== + + try: + subprocess.call([ + sys.executable, "-m", "python_appimage", "build", "app", + "-l", linuxTag, "-p", pythonVer, "appimage" + ], cwd=bldDir) + except Exception as exc: + print("AppImage build: FAILED") + print("") + print(str(exc)) + print("") + sys.exit(1) + + linuxLabel = "" + if not linuxTag.startswith("manylinux_2_28"): + linuxLabel = "-oldlinux" + if not linuxTag.endswith("x86_64"): + # manylinux_2_28 only comes in 64 bit, so this one only applies to older images + linuxLabel += "-32bit" + + bldFile = glob.glob(f"{bldDir}/*.AppImage")[0] + outFile = f"{bldDir}/novelWriter-{pkgVers}{linuxLabel}.AppImage" + os.rename(bldFile, outFile) + shaFile = makeCheckSum(os.path.basename(outFile), cwd=bldDir) + + toUpload(outFile) + toUpload(shaFile) + + return unparsedArgs + + +## +# Make Windows Setup EXE (build-win-exe) +## + +def makeWindowsEmbedded(sysArgs): + """Set up a package with embedded Python and dependencies for + Windows installation. + """ + import urllib.request + import zipfile + 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 package as an argument") + sys.exit(1) + + packVersion = os.path.basename(minimalZip).split("-")[1] + print("Version: %s" % packVersion) + + # 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) + + # 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 + # ========================== + + 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) + + print("Extracting ...") + with zipfile.ZipFile(pyZip, "r") as inFile: + inFile.extractall(outDir) + + print("Done") + print("") + + # Sort Out Licence Files + # ====================== + + 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("Install dependencies ...") + + sysCmd = [sys.executable] + sysCmd += "-m pip install -r requirements.txt --target".split() + sysCmd += [libDir] + try: + subprocess.call(sysCmd) + except Exception as exc: + print("Failed with error:") + print(str(exc)) + sys.exit(1) + + print("Done") + print("") + + # Update Launch File + # ================== + + print("Updating starting script ...") + + writeFile(os.path.join(outDir, "novelWriter.pyw"), ( + "#!/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): + os.unlink(delFile) + print("Deleted: %s" % delFile) + + def deleteFolder(delPath): + if os.path.isdir(delPath): + shutil.rmtree(delPath) + print("Deleted: %s" % delPath) + + print("Deleting Redundant Files") + print("========================") + print("") + + 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") + qmDir = os.path.join(qt5Dir, "translations") + dictDir = os.path.join(libDir, "enchant", "data", "mingw64", "share", "enchant", "hunspell") + + for dictFile in os.listdir(dictDir): + if not dictFile.startswith(("en_GB", "en_US")): + unlinkIfFound(os.path.join(dictDir, dictFile)) + + for qmFile in os.listdir(qmDir): + if not qmFile.startswith("qtbase"): + unlinkIfFound(os.path.join(qmDir, qmFile)) + + 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" + ] + for qt5Item in delQt5: + 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)) + + delList = [ + os.path.join(binDir, "opengl32sw.dll"), + 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 delItem in delList: + if os.path.isfile(delItem): + unlinkIfFound(delItem) + elif os.path.isdir(delItem): + deleteFolder(delItem) + + 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) + + print("") + print("Done") + print("") + + return + + +# =============================================================================================== # +# General Installers +# =============================================================================================== # + +## +# XDG Installation (xdg-install) +## + +def xdgInstall(): + """Will attempt to install icons and make a launcher.""" + print("") + print("XDG Install") + print("===========") + print("") + + # Find Executable(s) + # ================== + + exOpts = [] + + testExec = shutil.which("novelWriter") + if testExec is not None: + exOpts.append(testExec) + + testExec = shutil.which("novelwriter") + if testExec is not None: + exOpts.append(testExec) + + testExec = os.path.join(os.getcwd(), "novelWriter.py") + if os.path.isfile(testExec): + exOpts.append(testExec) + + useExec = "" + nOpts = len(exOpts) + if nOpts == 0: + print("Error: No executables for novelWriter found.") + sys.exit(1) + elif nOpts == 1: + useExec = exOpts[0] + else: + print("Found multiple novelWriter executables:") + print("") + for iExec, anExec in enumerate(exOpts): + print(" [%d] %s" % (iExec, anExec)) + print("") + intVal = int(input("Please select which novelWriter executable to use: ")) + print("") + + if intVal >= 0 and intVal < nOpts: + useExec = exOpts[intVal] + else: + print("Error: Invalid selection.") + sys.exit(1) + + print("Using executable: %s " % useExec) + print("") + + # Create and Install Launcher + # =========================== + + # Generate launcher + desktopData = readFile(os.path.join("setup", "data", "novelwriter.desktop")) + desktopData = desktopData.replace("Exec=novelwriter", f"Exec={useExec}") + writeFile("novelwriter.desktop", desktopData) + + # Remove old desktop icon + exCode = subprocess.call( + ["xdg-desktop-icon", "uninstall", "novelwriter.desktop"] + ) + + # Install application launcher + exCode = subprocess.call( + ["xdg-desktop-menu", "install", "--novendor", "novelwriter.desktop"] + ) + if exCode == 0: + print("Installed menu launcher file") + else: + print(f"Error {exCode}: Could not install menu launcher file") + + # Install MimeType + # ================ + + exCode = subprocess.call([ + "xdg-mime", "install", "setup/data/x-novelwriter-project.xml" + ]) + if exCode == 0: + print("Installed mimetype") + else: + print(f"Error {exCode}: Could not install mimetype") + + # Install Icons + # ============= + + iconRoot = "setup/data/hicolor" + sizeArr = ["16", "24", "32", "48", "64", "128", "256"] + + # App Icon + for aSize in sizeArr: + exCode = subprocess.call([ + "xdg-icon-resource", "install", "--novendor", "--noupdate", + "--context", "apps", "--size", aSize, + f"{iconRoot}/{aSize}x{aSize}/apps/novelwriter.png", + "novelwriter" + ]) + if exCode == 0: + print(f"Installed app icon size {aSize}") + else: + print(f"Error {exCode}: Could not install app icon size {aSize}") + + # Mimetype + for aSize in sizeArr: + exCode = subprocess.call([ + "xdg-icon-resource", "install", "--noupdate", + "--context", "mimetypes", "--size", aSize, + f"{iconRoot}/{aSize}x{aSize}/mimetypes/application-x-novelwriter-project.png", + "application-x-novelwriter-project" + ]) + if exCode == 0: + print(f"Installed mime icon size {aSize}") + else: + print(f"Error {exCode}: Could not install mime icon size {aSize}") + + # Update Cache + exCode = subprocess.call(["xdg-icon-resource", "forceupdate"]) + if exCode == 0: + print("Updated icon cache") + else: + print(f"Error {exCode}: Could not update icon cache") + + # Clean up + if os.path.isfile("./novelwriter.desktop"): + os.unlink("./novelwriter.desktop") + + print("") + print("Done!") + print("") + + return + + +## +# XDG Uninstallation (xdg-uninstall) +## + +def xdgUninstall(): + """Will attempt to uninstall icons and the launcher.""" + print("") + print("XDG Uninstall") + print("=============") + print("") + + # Application Menu Icon + exCode = subprocess.call( + ["xdg-desktop-menu", "uninstall", "novelwriter.desktop"] + ) + if exCode == 0: + print("Uninstalled menu launcher file") + else: + print(f"Error {exCode}: Could not uninstall menu launcher file") + + # Desktop Icon + # (No longer installed) + exCode = subprocess.call( + ["xdg-desktop-icon", "uninstall", "novelwriter.desktop"] + ) + if exCode == 0: + print("Uninstalled desktop launcher file") + else: + print(f"Error {exCode}: Could not uninstall desktop launcher file") + + # Also include no longer used sizes + sizeArr = ["16", "22", "24", "32", "48", "64", "96", "128", "256", "512"] + + # App Icons + for aSize in sizeArr: + exCode = subprocess.call([ + "xdg-icon-resource", "uninstall", "--noupdate", + "--context", "apps", "--size", aSize, "novelwriter" + ]) + if exCode == 0: + print(f"Uninstalled app icon size {aSize}") + else: + print(f"Error {exCode}: Could not uninstall app icon size {aSize}") + + # Mimetype + for aSize in sizeArr: + exCode = subprocess.call([ + "xdg-icon-resource", "uninstall", "--noupdate", + "--context", "mimetypes", "--size", aSize, + "application-x-novelwriter-project" + ]) + if exCode == 0: + print(f"Uninstalled mime icon size {aSize}") + else: + print(f"Error {exCode}: Could not uninstall mime icon size {aSize}") + + # Update Cache + exCode = subprocess.call(["xdg-icon-resource", "forceupdate"]) + if exCode == 0: + print("Updated icon cache") + else: + print(f"Error {exCode}: Could not update icon cache") + + print("") + print("Done!") + print("") + + return + + +## +# WIN Installation (win-install) +## + +def winInstall(): + """Will attempt to install icons and make a launcher for Windows.""" + import winreg + try: + import win32com.client + except ImportError: + print( + "ERROR: Package 'pywin32' is missing on this system.\n" + " Please run 'pkgutils.py pip' to automatically install\n" + " dependecies, or run 'pip install --user pywin32'." + ) + sys.exit(1) + + print("") + print("Windows Install") + print("===============") + print("") + + numVers, hexVers, _ = extractVersion() + nwTesting = not hexVers[-2] == "f" + wShell = win32com.client.Dispatch("WScript.Shell") + + if nwTesting: + linkName = "novelWriter Testing %s.lnk" % numVers + else: + linkName = "novelWriter %s.lnk" % numVers + + desktopDir = wShell.SpecialFolders("Desktop") + desktopIcon = os.path.join(desktopDir, linkName) + + startMenuDir = wShell.SpecialFolders("StartMenu") + startMenuProg = os.path.join(startMenuDir, "Programs", "novelWriter") + startMenuIcon = os.path.join(startMenuProg, linkName) + + pythonDir = os.path.dirname(sys.executable) + pythonExe = os.path.join(pythonDir, "pythonw.exe") + + targetDir = os.path.abspath(os.path.dirname(__file__)) + targetPy = os.path.join(targetDir, "novelWriter.pyw") + targetIcon = os.path.join(targetDir, "novelwriter", "assets", "icons", "novelwriter.ico") + + if not os.path.isfile(targetPy): + shutil.copy2(os.path.join(targetDir, "novelWriter.py"), targetPy) + + print("") + print("Collecting Info ...") + print("Desktop Folder: %s" % desktopDir) + print("Start Menu Folder: %s" % startMenuDir) + print("Python Executable: %s" % pythonExe) + print("Target Executable: %s" % targetPy) + print("Target Icon: %s" % targetIcon) + print("") + + print("Creating Links ...") + if os.path.isfile(desktopIcon): + os.unlink(desktopIcon) + print("Deleted: %s" % desktopIcon) + + if os.path.isdir(startMenuProg): + for oldIcon in os.listdir(startMenuProg): + oldPath = os.path.join(startMenuProg, oldIcon) + if not oldIcon.startswith("novelWriter"): + continue + + isTesting = oldIcon.startswith("novelWriter Testing") + if isTesting and nwTesting: + os.unlink(oldPath) + print("Deleted: %s" % oldPath) + if not isTesting and not nwTesting: + os.unlink(oldPath) + print("Deleted: %s" % oldPath) + + else: + os.mkdir(startMenuProg) + print("Created: %s" % startMenuProg) + + wShortcut = wShell.CreateShortCut(desktopIcon) + wShortcut.TargetPath = targetPy + wShortcut.WorkingDirectory = targetDir + wShortcut.IconLocation = targetIcon + wShortcut.WindowStyle = 1 + wShortcut.save() + print("Created: %s" % desktopIcon) + + wShortcut = wShell.CreateShortCut(startMenuIcon) + wShortcut.TargetPath = targetPy + wShortcut.WorkingDirectory = targetDir + wShortcut.IconLocation = targetIcon + wShortcut.WindowStyle = 1 + wShortcut.save() + print("Created: %s" % startMenuIcon) + + print("") + print("Creating registry keys ...") + + def setKey(kPath, kName, kVal): + winreg.CreateKey(winreg.HKEY_CURRENT_USER, kPath) + regKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, kPath, 0, winreg.KEY_WRITE) + winreg.SetValueEx(regKey, kName, 0, winreg.REG_SZ, kVal) + winreg.CloseKey(regKey) + + mimeIcon = os.path.join( + targetDir, "novelwriter", "assets", "icons", "x-novelwriter-project.ico" + ) + mimeExec = '"%s" "%s" "%%1"' % (pythonExe, targetPy) + + try: + setKey(r"Software\Classes\.nwx\OpenWithProgids", "novelWriterProject.nwx", "") + setKey(r"Software\Classes\novelWriterProject.nwx", "", "novelWriter Project File") + setKey(r"Software\Classes\novelWriterProject.nwx\DefaultIcon", "", mimeIcon) + setKey(r"Software\Classes\novelWriterProject.nwx\shell\open\command", "", mimeExec) + setKey(r"Software\Classes\Applications\novelWriter.pyw\SupportedTypes", ".nwx", "") + except WindowsError: + print("ERROR: Failed to set registry keys.") + print("") + + print("") + print("Done!") + print("") + + return + + +## +# WIN Uninstallation (win-uninstall) +## + +def winUninstall(): + """Will attempt to uninstall icons previously installed.""" + import winreg + try: + import win32com.client + except ImportError: + print( + "ERROR: Package 'pywin32' is missing on this system.\n" + " Please run 'pip install --user pywin32' to install it." + ) + sys.exit(1) + + print("") + print("Windows Uninstall") + print("=================") + print("") + + numVers, hexVers, _ = extractVersion() + nwTesting = not hexVers[-2] == "f" + wShell = win32com.client.Dispatch("WScript.Shell") + + if nwTesting: + linkName = "novelWriter Testing %s.lnk" % numVers + else: + linkName = "novelWriter %s.lnk" % numVers + + desktopDir = wShell.SpecialFolders("Desktop") + desktopIcon = os.path.join(desktopDir, linkName) + + startMenuDir = wShell.SpecialFolders("StartMenu") + startMenuProg = os.path.join(startMenuDir, "Programs", "novelWriter") + startMenuIcon = os.path.join(startMenuProg, linkName) + + print("") + print("Deleting Links ...") + if os.path.isfile(desktopIcon): + os.unlink(desktopIcon) + print("Deleted: %s" % desktopIcon) + else: + print("Not Found: %s" % desktopIcon) + + if os.path.isfile(startMenuIcon): + os.unlink(startMenuIcon) + print("Deleted: %s" % startMenuIcon) + else: + print("Not Found: %s" % startMenuIcon) + + if os.path.isdir(startMenuProg): + if not os.listdir(startMenuProg): + os.rmdir(startMenuProg) + print("Deleted: %s" % startMenuProg) + else: + print("Not Empty: %s" % startMenuProg) + + print("") + print("Removing registry keys ...") + + theKeys = [ + r"Software\Classes\novelWriterProject.nwx\shell\open\command", + r"Software\Classes\novelWriterProject.nwx\shell\open", + r"Software\Classes\novelWriterProject.nwx\shell", + r"Software\Classes\novelWriterProject.nwx\DefaultIcon", + r"Software\Classes\novelWriterProject.nwx", + r"Software\Classes\.nwx\OpenWithProgids", + r"Software\Classes\.nwx", + r"Software\Classes\Applications\novelWriter.pyw\SupportedTypes", + r"Software\Classes\Applications\novelWriter.pyw", + ] + + for aKey in theKeys: + try: + winreg.DeleteKey(winreg.HKEY_CURRENT_USER, aKey) + print("Deleted: HKEY_CURRENT_USER\\%s" % aKey) + except WindowsError: + print("Not Found: HKEY_CURRENT_USER\\%s" % aKey) + + print("") + print("Done!") + print("") + + 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 + 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 + + # Set Target OS + if "--target-linux" in sys.argv: + sys.argv.remove("--target-linux") + targetOS = OS_LINUX + elif "--target-darwin" in sys.argv: + sys.argv.remove("--target-darwin") + targetOS = OS_DARWIN + elif "--target-win" in sys.argv: + sys.argv.remove("--target-win") + targetOS = OS_WIN + else: + targetOS = hostOS + + # Sign package + if "--sign" in sys.argv: + sys.argv.remove("--sign") + doSign = True + else: + doSign = False + + # First build + if "--first" in sys.argv: + sys.argv.remove("--first") + isFirstBuild = True + else: + isFirstBuild = False + + # Build snapshot + if "--snapshot" in sys.argv: + sys.argv.remove("--snapshot") + isSnapshot = True + else: + isSnapshot = False + + helpMsg = [ + "", + "novelWriter Setup Tool", + "======================", + "", + "This tool provides setup and build commands for installing or distibuting", + "novelWriter as a package on Linux, Mac and Windows. The available options", + "are as follows:", + "", + "Some of the commands can be targeted towards a different OS than the host OS.", + "To target the command, add one of '--target-linux', '--target-darwin' or", + "'--target-win'.", + "", + "General:", + "", + " help Print the help message.", + " pip Install all package dependencies for novelWriter using pip.", + " version Print the novelWriter version.", + " build-clean Will attempt to delete 'build' and 'dist' folders.", + "", + "Additional Builds:", + "", + " manual Build the help documentation as PDF (requires LaTeX).", + " sample Build the sample project zip file and add it to assets.", + " qtlupdate Update translation files for internationalisation.", + " The files to be updated must be provided as arguments.", + " qtlrelease Build the language files for internationalisation.", + " clean-assets Delete assets built by manual, sample and qtlrelease.", + " gen-plist Generates an Info.plist for use in a MacOS Bundle", + "", + "Python Packaging:", + "", + " import-i18n Import updated i18n files from a zip file.", + " minimal-zip Creates a minimal zip file of the core application without", + " all the other source files. Defaults to tailor the zip file", + " for the current OS, but accepts a target OS flag to build", + " for another OS.", + " build-deb Build a .deb package for Debian and Ubuntu. Add --sign to ", + " sign package.", + " 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-win-exe Build a setup.exe file with Python embedded for Windows.", + " The package must be built from a minimal windows zip file.", + " build-appimage Build an AppImage. Argument --linux-tag defaults to", + " manylinux_2_28_x86_64, and --python-version to 3.11.", + "", + "System Install:", + "", + " install Deprecated. Use pip or build.", + " 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.", + " xdg-uninstall Remove the launcher and icons for the current system as", + " installed by the 'xdg-install' command.", + " win-install Install desktop icon, start menu icon, and registry entries", + " for file association with .nwx files for Windows systems.", + " win-uninstall Remove desktop icon, start menu icon, and registry keys,", + " for the current system. Note that it only removes icons for", + " the version number of the package the command is run from.", + "", + ] + + # General + # ======= + + if "help" in sys.argv: + sys.argv.remove("help") + print("\n".join(helpMsg)) + sys.exit(0) + + if "version" in sys.argv: + sys.argv.remove("version") + numVers, _, _ = extractVersion(beQuiet=True) + print(numVers, end=None) + sys.exit(0) + + if "pip" in sys.argv: + sys.argv.remove("pip") + installPackages(hostOS) + + if "build-clean" in sys.argv: + sys.argv.remove("build-clean") + cleanBuildDirs() + + # Additional Builds + # ================= + + if "manual" in sys.argv: + sys.argv.remove("manual") + buildPdfManual() + + if "qtlrelease" in sys.argv: + sys.argv.remove("qtlrelease") + buildQtI18n() + + if "qtlupdate" in sys.argv: + sys.argv.remove("qtlupdate") + buildQtI18nTS(sys.argv) + sys.exit(0) # Don't continue execution + + if "sample" in sys.argv: + sys.argv.remove("sample") + buildSampleZip() + + if "clean-assets" in sys.argv: + sys.argv.remove("clean-assets") + cleanBuiltAssets() + + if "gen-plist" in sys.argv: + sys.argv.remove("gen-plist") + genMacOSPlist() + + # Python Packaging + # ================ + + if "import-i18n" in sys.argv: + sys.argv.remove("import-i18n") + importI18nUpdates(sys.argv) + sys.exit(0) # Don't continue execution + + if "minimal-zip" in sys.argv: + sys.argv.remove("minimal-zip") + makeMinimalPackage(targetOS) + + if "build-deb" in sys.argv: + sys.argv.remove("build-deb") + if hostOS == OS_LINUX: + if doSign: + signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" + else: + signKey = None + makeDebianPackage(signKey=signKey) + else: + print("ERROR: Command 'build-deb' can only be used on Linux") + sys.exit(1) + + if "build-ubuntu" in sys.argv: + sys.argv.remove("build-ubuntu") + if hostOS == OS_LINUX: + makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild, isSnapshot=isSnapshot) + else: + print("ERROR: Command 'build-ubuntu' can only be used on Linux") + sys.exit(1) + + if "build-win-exe" in sys.argv: + sys.argv.remove("build-win-exe") + makeWindowsEmbedded(sys.argv) + sys.exit(0) # Don't continue execution + + if "build-appimage" in sys.argv: + sys.argv.remove("build-appimage") + if hostOS == OS_LINUX: + sys.argv = makeAppImage(sys.argv) # Build appimage and prune its args + else: + print("ERROR: Command 'build-appimage' can only be used on Linux") + sys.exit(1) + + # General Installers + # ================== + + if "xdg-install" in sys.argv: + sys.argv.remove("xdg-install") + if hostOS == OS_WIN: + print("ERROR: Command 'xdg-install' cannot be used on Windows") + sys.exit(1) + else: + xdgInstall() + + if "xdg-uninstall" in sys.argv: + sys.argv.remove("xdg-uninstall") + if hostOS == OS_WIN: + print("ERROR: Command 'xdg-uninstall' cannot be used on Windows") + sys.exit(1) + else: + xdgUninstall() + + if "win-install" in sys.argv: + sys.argv.remove("win-install") + if hostOS == OS_WIN: + winInstall() + else: + print("ERROR: Command 'win-install' can only be used on Windows") + sys.exit(1) + + if "win-uninstall" in sys.argv: + sys.argv.remove("win-uninstall") + if hostOS == OS_WIN: + winUninstall() + else: + print("ERROR: Command 'win-uninstall' can only be used on Windows") + sys.exit(1) + +# END Main diff --git a/setup.py b/setup.py index 6d3b8c79..c83c9112 100755 --- a/setup.py +++ b/setup.py @@ -1,2075 +1,13 @@ #!/usr/bin/env python3 -""" -novelWriter – Main Setup Script -=============================== -The main setup and install script for all operating systems -File History: -Created: 2019-05-16 [0.5.1] +import setuptools # noqa: F401 +import warnings # noqa: F401 -This file is a part of novelWriter -Copyright 2018–2023, Veronica Berglyd Olsen +warnings.warn( + "The setuptools style setup.py script is deprecated and will be " + "removed in a future release. Please don't use the 'setup.py install' " + "command, and instead use the pip and build commands.", + DeprecationWarning +) -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 . -""" - -import os -import sys -import shutil -import zipfile -import datetime -import subprocess -import email.utils - -OS_NONE = 0 -OS_LINUX = 1 -OS_WIN = 2 -OS_DARWIN = 3 - - -# =============================================================================================== # -# Utilities -# =============================================================================================== # - -def extractVersion(beQuiet=False): - """Extract the novelWriter version number without having to import - anything else from the main package. - """ - def getValue(theString): - theBits = theString.partition("=") - return theBits[2].strip().strip('"') - - numVers = "Unknown" - hexVers = "Unknown" - relDate = "Unknown" - initFile = os.path.join("novelwriter", "__init__.py") - try: - with open(initFile, mode="r", encoding="utf-8") as inFile: - for aLine in inFile: - if aLine.startswith("__version__"): - numVers = getValue((aLine)) - if aLine.startswith("__hexversion__"): - hexVers = getValue((aLine)) - if aLine.startswith("__date__"): - relDate = getValue((aLine)) - except Exception as exc: - print("Could not read file: %s" % initFile) - print(str(exc)) - - if not beQuiet: - print("novelWriter version: %s (%s) at %s" % (numVers, hexVers, relDate)) - - return numVers, hexVers, relDate - - -def compactVersion(version): - """Make the version number more compact. - """ - return version.replace("-alpha", "a").replace("-beta", "b").replace("-rc", "rc") - - -def sysCall(callArgs, cwd=None): - """Wrapper function for system calls. - """ - sysP = subprocess.Popen( - callArgs, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True, cwd=cwd - ) - stdOut, stdErr = sysP.communicate() - return stdOut.decode("utf-8"), stdErr.decode("utf-8"), sysP.returncode - - -def readFile(fileName): - """Read an entire file and return as a string. - """ - with open(fileName, mode="r", encoding="utf-8") as inFile: - return inFile.read() - - -def writeFile(fileName, writeText): - """Write string to file. - """ - with open(fileName, mode="w+", encoding="utf-8") as outFile: - outFile.write(writeText) - - -def toUpload(srcPath, dstName=None): - """Copy a file produced by one of the build functions to the uplaod - directory. The file can optionally be given a new name. - """ - uplDir = "dist_upload" - if not os.path.isdir(uplDir): - os.mkdir(uplDir) - if dstName is None: - dstName = os.path.basename(srcPath) - shutil.copyfile(srcPath, os.path.join(uplDir, dstName)) - return - - -def makeCheckSum(sumFile, cwd=None): - """Create a SHA256 checkcusm file. - """ - try: - if cwd is None: - shaFile = sumFile+".sha256" - else: - shaFile = os.path.join(cwd, sumFile+".sha256") - with open(shaFile, mode="w") as fOut: - subprocess.call(["shasum", "-a", "256", sumFile], stdout=fOut, cwd=cwd) - print("SHA256 Sum: %s" % shaFile) - except Exception as exc: - print("Could not generate sha256 file") - print(str(exc)) - return "" - - return shaFile - - -# =============================================================================================== # -# 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") - elif hostOS == OS_WIN: - installQueue.append("pywin32") - - 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 exc: - print("Failed with error:") - print(str(exc)) - sys.exit(1) - - return - - -## -# Clean Build and Dist Folders (build-clean) -## - -def cleanBuildDirs(): - """Recursively delete the 'build' and 'dist' folders. - """ - print("") - print("Cleaning up build environment ...") - print("") - - def removeFolder(rmDir): - if os.path.isdir(rmDir): - try: - shutil.rmtree(rmDir) - print("Deleted: %s" % rmDir) - except OSError: - print("Failed: %s" % rmDir) - else: - print("Missing: %s" % rmDir) - - removeFolder("build") - removeFolder("dist") - removeFolder("dist_deb") - removeFolder("dist_minimal") - removeFolder("dist_appimage") - removeFolder("novelWriter.egg-info") - - print("") - - return - - -# =============================================================================================== # -# Additional Buiilds -# =============================================================================================== # - -## -# Build PDF Manual (manual) -## - -def buildPdfManual(): - """This function will build the documentation as manual.pdf. - """ - print("") - print("Building PDF Manual") - print("===================") - print("") - - buildFile = os.path.join("docs", "build", "latex", "manual.pdf") - finalFile = os.path.join("novelwriter", "assets", "manual.pdf") - - if os.path.isfile(finalFile): - # Make sure a new file is always generated - os.unlink(finalFile) - - try: - subprocess.call(["make", "clean"], cwd="docs") - exCode = subprocess.call(["make", "latexpdf"], cwd="docs") - if exCode == 0: - if os.path.isfile(finalFile): - os.unlink(finalFile) - print("") - os.rename(buildFile, finalFile) - else: - raise Exception(f"Build returned error code {exCode}") - - print("PDF manual build: OK") - print("") - - except Exception as exc: - print("PDF manual build: FAILED") - print("") - print(str(exc)) - print("") - print("Dependencies:") - print(" * pip install sphinx") - print(" * Package latexmk") - print(" * LaTeX build system") - print("") - print(" On Debian/Ubuntu, install: python3-sphinx latexmk texlive texlive-latex-extra") - print("") - sys.exit(1) - - if not os.path.isfile(finalFile): - print("No output file was found!") - print("") - sys.exit(1) - - return - - -## -# Qt Linguist QM Builder (qtlrelease) -## - -def buildQtI18n(): - """Build the lang.qm files for Qt Linguist. - """ - print("") - print("Building Qt Localisation Files") - print("==============================") - - print("") - print("TS Files to Build:") - print("") - - tsList = [] - for aFile in os.listdir("i18n"): - aPath = os.path.join("i18n", aFile) - if os.path.isfile(aPath) and aFile.endswith(".ts") and aFile != "nw_base.ts": - tsList.append(aPath) - print(aPath) - - print("") - print("Building Translation Files:") - print("") - - try: - subprocess.call(["lrelease", "-verbose", *tsList]) - except Exception as exc: - print("Qt5 Linguist tools seem to be missing") - print("On Debian/Ubuntu, install: qttools5-dev-tools pyqt5-dev-tools") - print(str(exc)) - sys.exit(1) - - print("") - print("Moving QM Files to Assets") - print("") - - langDir = os.path.join("novelwriter", "assets", "i18n") - for langFile in os.listdir("i18n"): - langPath = os.path.join("i18n", langFile) - if not os.path.isfile(langPath): - continue - - if langFile.endswith(".qm"): - destPath = os.path.join(langDir, langFile) - os.rename(langPath, destPath) - print("Moved: %s -> %s" % (langPath, destPath)) - - print("") - - return - - -## -# Qt Linguist TS Builder (qtlupdate) -## - -def buildQtI18nTS(sysArgs): - """Build the lang.ts files for Qt Linguist. - """ - print("") - print("Building Qt Translation Files") - print("=============================") - - try: - from PyQt6.lupdate import lupdate - except ImportError: - print("ERROR: This command requires lupdate from PyQt6") - sys.exit(1) - - print("") - print("Scanning Source Tree:") - print("") - - srcList = [os.path.join("i18n", "qtbase.py")] - for nRoot, _, nFiles in os.walk("novelwriter"): - if os.path.isdir(nRoot): - for aFile in nFiles: - aPath = os.path.join(nRoot, aFile) - if os.path.isfile(aPath) and aFile.endswith(".py"): - srcList.append(aPath) - - for aSource in srcList: - print(aSource) - - print("") - print("TS Files to Update:") - print("") - - tsList = [] - if len(sysArgs) >= 2: - for anArg in sysArgs[1:]: - if not (anArg.startswith("i18n") and anArg.endswith(".ts")): - continue - - fName = os.path.basename(anArg) - if not fName.startswith("nw_") and len(fName) > 6: - print("Skipping non-novelWriter TS file %s" % fName) - continue - - if os.path.isfile(anArg): - tsList.append(anArg) - elif os.path.exists(anArg): - pass - else: # Create an empty new language file - lCode = fName[3:-3] - writeFile(anArg, ( - "\n" - "\n" - f"\n" - )) - tsList.append(anArg) - - else: - print("No translation files selected for update ...") - print("") - return - - for aTS in tsList: - print(aTS) - - print("") - print("Updating Language Files:") - print("") - - # Using the pylupdate tool from PyQt6 as it supports TS file format 2.1. - lupdate(srcList, tsList, no_obsolete=True, no_summary=False) - - print("") - - return - - -## -# Generage MacOS PList -## - -def genMacOSPlist(): - """Set necessary values for .plist file for MacOS build. - """ - numVers, _, _ = extractVersion() - pkgVers = compactVersion(numVers) - outDir = "setup/macos" - - copyrightYear = datetime.datetime.now().year - - # These keys are no longer used but are present for compatability - pkgVersMaj, pkgVersMin, _ = pkgVers.split(".") - - plistXML = readFile(f"{outDir}/Info.plist.template").format( - macosBundleSVers=pkgVers, - macosBundleVers=numVers, - macosBundleVersMajor=pkgVersMaj, - macosBundleVersMinor=pkgVersMin, - macosBundleCopyright=f"Copyright 2018–{copyrightYear}, Veronica Berglyd Olsen", - ) - - print(f"Writing Info.plist to {outDir}/Info.plist") - writeFile(f"{outDir}/Info.plist", plistXML) - - return - - -## -# Sample Project ZIP File Builder (sample) -## - -def buildSampleZip(): - """Bundle the sample project into a single zip file to be saved into - the novelwriter/assets folder for further bundling into builds. - """ - print("") - print("Building Sample ZIP File") - print("========================") - print("") - - srcSample = "sample" - dstSample = os.path.join("novelwriter", "assets", "sample.zip") - - if os.path.isdir(srcSample): - if os.path.isfile(dstSample): - os.unlink(dstSample) - - from zipfile import ZipFile - - with ZipFile(dstSample, "w") as zipObj: - print("Compressing: nwProject.nwx") - zipObj.write(os.path.join(srcSample, "nwProject.nwx"), "nwProject.nwx") - for docFile in os.listdir(os.path.join(srcSample, "content")): - print("Compressing: content/%s" % docFile) - srcDoc = os.path.join(srcSample, "content", docFile) - zipObj.write(srcDoc, "content/"+docFile) - - else: - print("Error: Could not find sample project source directory.") - sys.exit(1) - - print("") - print("Built file: %s" % dstSample) - print("") - - return - - -def cleanBuiltAssets(): - """Remove assets built by this script. - """ - print("") - print("Removing Built Assets") - print("=====================") - print("") - - sampleZip = os.path.join("novelwriter", "assets", "sample.zip") - if os.path.isfile(sampleZip): - print(f"Deleted: {sampleZip}") - os.unlink(sampleZip) - - pdfManual = os.path.join("novelwriter", "assets", "manual.pdf") - if os.path.isfile(pdfManual): - print(f"Deleted: {pdfManual}") - os.unlink(pdfManual) - - i18nAssets = os.path.join("novelwriter", "assets", "i18n") - for i18nItem in os.listdir(i18nAssets): - i18nPath = os.path.join(i18nAssets, i18nItem) - if os.path.isfile(i18nPath) and i18nPath.endswith(".qm"): - print(f"Deleted: {i18nPath}") - os.unlink(i18nPath) - - print("") - - return - - -def checkAssetsExist(): - """Check that the necessary compiled assets exist ahead of a build. - """ - hasSample = False - hasManual = False - hasQmData = False - - sampleZip = os.path.join("novelwriter", "assets", "sample.zip") - if os.path.isfile(sampleZip): - print(f"Found: {sampleZip}") - hasSample = True - - pdfManual = os.path.join("novelwriter", "assets", "manual.pdf") - if os.path.isfile(pdfManual): - print(f"Found: {pdfManual}") - hasManual = True - - i18nAssets = os.path.join("novelwriter", "assets", "i18n") - for i18nItem in os.listdir(i18nAssets): - i18nPath = os.path.join(i18nAssets, i18nItem) - if os.path.isfile(i18nPath) and i18nPath.endswith(".qm"): - print(f"Found: {i18nPath}") - hasQmData = True - - return hasSample and hasManual and hasQmData - - -# =============================================================================================== # -# Python Packaging -# =============================================================================================== # - -## -# Import Translations (import-i18n) -## - -def importI18nUpdates(sysArgs): - """Import new translation files from a zip file. - """ - print("") - print("Import Updated Translations") - print("===========================") - print("") - - fileName = None - if len(sysArgs) >= 2: - if os.path.isfile(sysArgs[1]): - fileName = sysArgs[1] - - if fileName is None: - print("File not found ...") - sys.exit(1) - - projPath = os.path.join("novelwriter", "assets", "i18n") - mainPath = "i18n" - - print("Loading file: %s" % fileName) - with zipfile.ZipFile(fileName) as zipObj: - for archFile in zipObj.namelist(): - if archFile.startswith("nw_") and archFile.endswith(".ts"): - zipObj.extract(archFile, mainPath) - print("Extracted: %s > %s" % (archFile, os.path.join(mainPath, archFile))) - elif archFile.startswith("project_") and archFile.endswith(".json"): - zipObj.extract(archFile, projPath) - print("Extracted: %s > %s" % (archFile, os.path.join(projPath, archFile))) - else: - print("Skipped: %s" % archFile) - - print("") - - return - - -## -# Make Minimal Package (minimal-zip) -## - -def makeMinimalPackage(targetOS): - """Pack the core source file in a single zip file. - """ - from zipfile import ZipFile, ZIP_DEFLATED - - print("") - print("Building Minimal ZIP File") - print("=========================") - - bldDir = "dist_minimal" - if not os.path.isdir(bldDir): - os.mkdir(bldDir) - - if targetOS == OS_LINUX: - targName = "-linux" - print("Target OS: Linux") - elif targetOS == OS_DARWIN: - targName = "-darwin" - print("Target OS: Darwin") - elif targetOS == OS_WIN: - targName = "-win" - print("Target OS: Windows") - else: - targName = "" - print("") - - # Check Additional Assets - # ======================= - - if not checkAssetsExist(): - print("ERROR: Missing build assets") - sys.exit(1) - - # Build Minimal Zip - # ================= - - numVers, _, _ = extractVersion() - pkgVers = compactVersion(numVers) - zipFile = f"novelwriter-{pkgVers}-minimal{targName}.zip" - outFile = os.path.join(bldDir, zipFile) - if os.path.isfile(outFile): - os.unlink(outFile) - print("") - - rootFiles = [ - "README.md", - "LICENSE.md", - "CREDITS.md", - "CHANGELOG.md", - "requirements.txt", - "setup.py", - "setup.cfg", - "pyproject.toml", - ] - - with ZipFile(outFile, "w", compression=ZIP_DEFLATED, compresslevel=9) as zipObj: - - for nRoot, _, nFiles in os.walk("novelwriter"): - if nRoot.endswith("__pycache__"): - print("Skipped: %s" % nRoot) - continue - - print("Added: %s/* [Files: %d]" % (nRoot, len(nFiles))) - for aFile in nFiles: - if aFile.endswith(".pyc"): - print("Skipping File: %s" % aFile) - continue - zipObj.write(os.path.join(nRoot, aFile)) - - if targetOS == OS_WIN: - zipObj.write("novelWriter.py", "novelWriter.pyw") - print("Added: novelWriter.pyw") - zipObj.write(os.path.join("setup", "windows_install.bat"), "windows_install.bat") - print("Added: windows_install.bat") - zipObj.write(os.path.join("setup", "windows_uninstall.bat"), "windows_uninstall.bat") - print("Added: windows_uninstall.bat") - - else: # Linux and Mac - # Add icons - for nRoot, _, nFiles in os.walk(os.path.join("setup", "data")): - print("Added: %s/* [Files: %d]" % (nRoot, len(nFiles))) - for aFile in nFiles: - zipObj.write(os.path.join(nRoot, aFile)) - - zipObj.write(os.path.join("setup", "description_pypi.md")) - print("Added: setup/description_pypi.md") - - zipObj.write("novelWriter.py") - print("Added: novelWriter.py") - - for aFile in rootFiles: - print("Added: %s" % aFile) - zipObj.write(aFile) - - zipObj.write(os.path.join("novelwriter", "assets", "manual.pdf"), "UserManual.pdf") - print("Added: UserManual.pdf") - - print("") - print("Created File: %s" % outFile) - - # Create Checksum File - # ==================== - - shaFile = makeCheckSum(zipFile, cwd=bldDir) - - toUpload(outFile) - toUpload(shaFile) - - print("") - - return - - -## -# Make Debian Package (build-deb) -## - -def makeDebianPackage(signKey=None, sourceBuild=False, distName="unstable", buildName=""): - """Build a Debian package. - """ - print("") - print("Build Debian Package") - print("====================") - print("On Debian/Ubuntu install: dh-python python3-all debhelper devscripts") - print("") - - # Version Info - # ============ - - numVers, hexVers, relDate = extractVersion() - pkgVers = compactVersion(numVers) - relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") - pkgDate = email.utils.format_datetime(relDate.replace(hour=12, tzinfo=None)) - print("") - - if buildName: - pkgVers = f"{pkgVers}{buildName}" - - # Set Up Folder - # ============= - - bldDir = "dist_deb" - bldPkg = f"novelwriter_{pkgVers}" - outDir = f"{bldDir}/{bldPkg}" - debDir = f"{outDir}/debian" - datDir = f"{outDir}/data" - - if not os.path.isdir(bldDir): - os.mkdir(bldDir) - - if os.path.isdir(outDir): - print("Removing old build files ...") - print("") - shutil.rmtree(outDir) - - os.mkdir(outDir) - - # Check Additional Assets - # ======================= - - if not checkAssetsExist(): - print("ERROR: Missing build assets") - sys.exit(1) - - # Copy novelWriter Source - # ======================= - - print("Copying novelWriter source ...") - print("") - - for nPath, _, nFiles in os.walk("novelwriter"): - if nPath.endswith("__pycache__"): - print("Skipped: %s" % nPath) - continue - - pPath = f"{outDir}/{nPath}" - if not os.path.isdir(pPath): - os.mkdir(pPath) - - fCount = 0 - for fFile in nFiles: - nFile = f"{nPath}/{fFile}" - pFile = f"{pPath}/{fFile}" - - if fFile.endswith(".pyc"): - print("Skipped: %s" % nFile) - continue - - shutil.copyfile(nFile, pFile) - fCount += 1 - - print("Copied: %s/* [Files: %d]" % (nPath, fCount)) - - print("") - print("Copying or generating additional files ...") - print("") - - # Copy/Write Root Files - # ===================== - - copyFiles = ["LICENSE.md", "CREDITS.md", "CHANGELOG.md", "pyproject.toml"] - for copyFile in copyFiles: - shutil.copyfile(copyFile, f"{outDir}/{copyFile}") - print("Copied: %s" % copyFile) - - writeFile(f"{outDir}/MANIFEST.in", ( - "include LICENSE.md\n" - "include CREDITS.md\n" - "include CHANGELOG.md\n" - "include data/*\n" - "recursive-include novelwriter/assets *\n" - )) - print("Wrote: MANIFEST.in") - - writeFile(f"{outDir}/setup.py", ( - "import setuptools\n" - "setuptools.setup()\n" - )) - print("Wrote: setup.py") - - setupCfg = readFile("setup.cfg").replace( - "file: setup/description_pypi.md", "file: data/description_short.txt" - ) - writeFile(f"{outDir}/setup.cfg", setupCfg) - print("Wrote: setup.cfg") - - # Copy/Write Debian Files - # ======================= - - shutil.copytree("setup/debian", debDir) - print("Copied: debian/*") - - writeFile(f"{debDir}/changelog", ( - f"novelwriter ({pkgVers}) {distName}; urgency=low\n\n" - f" * Update to version {pkgVers}\n\n" - f" -- Veronica Berglyd Olsen {pkgDate}\n" - )) - print("Wrote: debian/changelog") - - # Copy/Write Data Files - # ===================== - - shutil.copytree("setup/data", datDir) - print("Copied: data/*") - - shutil.copyfile("setup/description_short.txt", f"{outDir}/data/description_short.txt") - print("Copied: data/description_short.txt") - - # Build Package - # ============= - - print("") - print("Running dpkg-buildpackage ...") - print("") - - if signKey is None: - signArgs = ["-us", "-uc"] - else: - signArgs = [f"-k{signKey}"] - - if sourceBuild: - subprocess.call(["debuild", "-S"] + signArgs, cwd=outDir) - toUpload(f"{bldDir}/{bldPkg}.tar.xz") - else: - subprocess.call(["dpkg-buildpackage"] + signArgs, cwd=outDir) - shutil.copyfile(f"{bldDir}/{bldPkg}.tar.xz", f"{bldDir}/{bldPkg}.debian.tar.xz") - toUpload(f"{bldDir}/{bldPkg}.debian.tar.xz") - toUpload(f"{bldDir}/{bldPkg}_all.deb") - - toUpload(makeCheckSum(f"{bldPkg}.debian.tar.xz", cwd=bldDir)) - toUpload(makeCheckSum(f"{bldPkg}_all.deb", cwd=bldDir)) - - print("") - print("Done!") - print("") - - if sourceBuild: - if hexVers[-2] == "f": - ppaName = "novelwriter" - else: - ppaName = "novelwriter-pre" - - return f"dput {ppaName}/{distName} {bldDir}/{bldPkg}_source.changes" - - return "" - - -## -# Make Launchpad Package (build-ubuntu) -## - -def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False): - """Wrapper for building debian packages for launchpad. - """ - print("") - print("Launchpad Packages") - print("==================") - print("") - - if isFirst or isSnapshot: - bldNum = "0" - else: - bldNum = input("Build number [0]: ") - if bldNum == "": - bldNum = "0" - - distLoop = [ - ("20.04", "focal"), - ("22.04", "jammy"), - ("22.10", "kinetic"), - ("23.04", "lunar"), - ] - - tStamp = datetime.datetime.now().strftime("%Y%m%d~%H%M%S") - if isSnapshot: - print(f"Building Ununtu SNAPSHOT~{tStamp} for:") - print("") - else: - print("Building Ubuntu packages for:") - print("") - for distNum, codeName in distLoop: - print(f" * Ubuntu {distNum} {codeName.title()}") - print("") - - if doSign: - signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" - else: - signKey = None - - print(f"Sign Key: {str(signKey)}") - print("") - - dputCmd = [] - for distNum, codeName in distLoop: - if isSnapshot: - buildName = f"+SNAPSHOT~{tStamp}~ubuntu{distNum}.0" - else: - buildName = f"~ubuntu{distNum}.{bldNum}" - - dCmd = makeDebianPackage( - signKey=signKey, - sourceBuild=True, - distName=codeName, - buildName=buildName - ) - dputCmd.append(dCmd) - - print("Packages Built") - print("==============") - print("") - for dCmd in dputCmd: - print(f" > {dCmd}") - print("") - - return - - -## -# Make AppImage (build-appimage) -## - -def makeAppImage(sysArgs): - """Build an AppImage. - """ - import glob - import argparse - - try: - import python_appimage # noqa F401 - except ImportError: - print( - "ERROR: Package 'python-appimage' is missing on this system.\n" - " Please run 'pip install --user python-appimage' to install it.\n" - ) - sys.exit(1) - - print("") - print("Build AppImage") - print("==============") - print("") - - parser = argparse.ArgumentParser( - prog="build_appimage", - description="Build an AppImage", - epilog="see https://appimage.org/ for more details", - ) - parser.add_argument( - "--linux-tag", - nargs="?", - default="manylinux_2_28_x86_64", - help=( - "Linux compatibility tag (e.g. manylinux_2_28_x86_64)\n" - "see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n" - "and https://github.com/pypa/manylinux for a list of valid tags" - ), - ) - parser.add_argument( - "--python-version", nargs="?", default="3.11", help="Python version (e.g. 3.11)" - ) - - args, unparsedArgs = parser.parse_known_args(sysArgs) - - linuxTag = args.linux_tag - pythonVer = args.python_version - - # Version Info - # ============ - - numVers, _, relDate = extractVersion() - pkgVers = compactVersion(numVers) - relDate = datetime.datetime.strptime(relDate, "%Y-%m-%d") - print("") - - # Set Up Folder - # ============= - - bldDir = "dist_appimage" - bldPkg = f"novelwriter_{pkgVers}" - outDir = f"{bldDir}/{bldPkg}" - imageDir = f"{bldDir}/appimage" - - # Set Up Folders - # ============== - - if not os.path.isdir(bldDir): - os.mkdir(bldDir) - - if os.path.isdir(outDir): - print("Removing old build files ...") - print("") - shutil.rmtree(outDir) - - os.mkdir(outDir) - - if os.path.isdir(imageDir): - print("Removing old build metadata files ...") - print("") - shutil.rmtree(imageDir) - - os.mkdir(imageDir) - - # Remove old AppImages - outFiles = glob.glob(f"{bldDir}/*.AppImage") - if outFiles: - print("Removing old AppImages") - print("") - for image in outFiles: - try: - os.remove(image) - except OSError: - print("Error while deleting file : ", image) - - # Copy novelWriter Source - # ======================= - - print("Copying novelWriter source ...") - print("") - - for nPath, _, nFiles in os.walk("novelwriter"): - if nPath.endswith("__pycache__"): - print("Skipped: %s" % nPath) - continue - - pPath = f"{outDir}/{nPath}" - if not os.path.isdir(pPath): - os.mkdir(pPath) - - fCount = 0 - for fFile in nFiles: - nFile = f"{nPath}/{fFile}" - pFile = f"{pPath}/{fFile}" - - if fFile.endswith(".pyc"): - print("Skipped: %s" % nFile) - continue - - shutil.copyfile(nFile, pFile) - fCount += 1 - - print("Copied: %s/* [Files: %d]" % (nPath, fCount)) - - print("") - print("Copying or generating additional files ...") - print("") - - # Copy/Write Root Files - # ===================== - - copyFiles = ["LICENSE.md", "CREDITS.md", "CHANGELOG.md", "pyproject.toml"] - for copyFile in copyFiles: - shutil.copyfile(copyFile, f"{outDir}/{copyFile}") - print("Copied: %s" % copyFile) - - writeFile(f"{outDir}/MANIFEST.in", ( - "include LICENSE.md\n" - "include CREDITS.md\n" - "include CHANGELOG.md\n" - "include data/*\n" - "recursive-include novelwriter/assets *\n" - )) - print("Wrote: MANIFEST.in") - - writeFile(f"{outDir}/setup.py", ( - "import setuptools\n" - "setuptools.setup()\n" - )) - print("Wrote: setup.py") - - setupCfg = readFile("setup.cfg").replace( - "file: setup/description_pypi.md", "file: data/description_short.txt" - ) - writeFile(f"{outDir}/setup.cfg", setupCfg) - print("Wrote: setup.cfg") - - # Write Metadata - # ============== - - appDescription = readFile("setup/description_short.txt") - appdataXML = readFile("setup/novelwriter.appdata.xml").format(description=appDescription) - writeFile(f"{imageDir}/novelwriter.appdata.xml", appdataXML) - print("Wrote: novelwriter.appdata.xml") - - writeFile(f"{imageDir}/entrypoint.sh", ( - '#! /bin/bash \n' - '{{ python-executable }} -sE ${APPDIR}/opt/python{{ python-version }}/bin/novelwriter "$@"' - )) - print("Wrote: entrypoint.sh") - - writeFile(f"{imageDir}/requirements.txt", os.path.abspath(outDir)) - print("Wrote: requirements.txt") - - shutil.copyfile("setup/data/novelwriter.desktop", f"{imageDir}/novelwriter.desktop") - print("Copied: novelwriter.desktop") - - shutil.copyfile("setup/icons/novelwriter.svg", f"{imageDir}/novelwriter.svg") - print("Copied: novelwriter.svg") - - shutil.copyfile( - "setup/data/hicolor/256x256/apps/novelwriter.png", f"{imageDir}/novelwriter.png" - ) - print("Copied: novelwriter.png") - - # Build AppImage - # ============== - - try: - subprocess.call([ - sys.executable, "-m", "python_appimage", "build", "app", - "-l", linuxTag, "-p", pythonVer, "appimage" - ], cwd=bldDir) - except Exception as exc: - print("AppImage build: FAILED") - print("") - print(str(exc)) - print("") - sys.exit(1) - - linuxLabel = "" - if not linuxTag.startswith("manylinux_2_28"): - linuxLabel = "-oldlinux" - if not linuxTag.endswith("x86_64"): - # manylinux_2_28 only comes in 64 bit, so this one only applies to older images - linuxLabel += "-32bit" - - bldFile = glob.glob(f"{bldDir}/*.AppImage")[0] - outFile = f"{bldDir}/novelWriter-{pkgVers}{linuxLabel}.AppImage" - os.rename(bldFile, outFile) - shaFile = makeCheckSum(os.path.basename(outFile), cwd=bldDir) - - toUpload(outFile) - toUpload(shaFile) - - return unparsedArgs - - -## -# Make Windows Setup EXE (build-win-exe) -## - -def makeWindowsEmbedded(sysArgs): - """Set up a package with embedded Python and dependencies for - Windows installation. - """ - import urllib.request - import zipfile - 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 package as an argument") - sys.exit(1) - - packVersion = os.path.basename(minimalZip).split("-")[1] - print("Version: %s" % packVersion) - - # 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) - - # 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 - # ========================== - - 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) - - print("Extracting ...") - with zipfile.ZipFile(pyZip, "r") as inFile: - inFile.extractall(outDir) - - print("Done") - print("") - - # Sort Out Licence Files - # ====================== - - 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("Install dependencies ...") - - sysCmd = [sys.executable] - sysCmd += "-m pip install -r requirements.txt --target".split() - sysCmd += [libDir] - try: - subprocess.call(sysCmd) - except Exception as exc: - print("Failed with error:") - print(str(exc)) - sys.exit(1) - - print("Done") - print("") - - # Update Launch File - # ================== - - print("Updating starting script ...") - - writeFile(os.path.join(outDir, "novelWriter.pyw"), ( - "#!/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): - os.unlink(delFile) - print("Deleted: %s" % delFile) - - def deleteFolder(delPath): - if os.path.isdir(delPath): - shutil.rmtree(delPath) - print("Deleted: %s" % delPath) - - print("Deleting Redundant Files") - print("========================") - print("") - - 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") - qmDir = os.path.join(qt5Dir, "translations") - dictDir = os.path.join(libDir, "enchant", "data", "mingw64", "share", "enchant", "hunspell") - - for dictFile in os.listdir(dictDir): - if not dictFile.startswith(("en_GB", "en_US")): - unlinkIfFound(os.path.join(dictDir, dictFile)) - - for qmFile in os.listdir(qmDir): - if not qmFile.startswith("qtbase"): - unlinkIfFound(os.path.join(qmDir, qmFile)) - - 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" - ] - for qt5Item in delQt5: - 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)) - - delList = [ - os.path.join(binDir, "opengl32sw.dll"), - 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 delItem in delList: - if os.path.isfile(delItem): - unlinkIfFound(delItem) - elif os.path.isdir(delItem): - deleteFolder(delItem) - - 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) - - print("") - print("Done") - print("") - - return - - -# =============================================================================================== # -# General Installers -# =============================================================================================== # - -## -# XDG Installation (xdg-install) -## - -def xdgInstall(): - """Will attempt to install icons and make a launcher. - """ - print("") - print("XDG Install") - print("===========") - print("") - - # Find Executable(s) - # ================== - - exOpts = [] - - testExec = shutil.which("novelWriter") - if testExec is not None: - exOpts.append(testExec) - - testExec = shutil.which("novelwriter") - if testExec is not None: - exOpts.append(testExec) - - testExec = os.path.join(os.getcwd(), "novelWriter.py") - if os.path.isfile(testExec): - exOpts.append(testExec) - - useExec = "" - nOpts = len(exOpts) - if nOpts == 0: - print("Error: No executables for novelWriter found.") - sys.exit(1) - elif nOpts == 1: - useExec = exOpts[0] - else: - print("Found multiple novelWriter executables:") - print("") - for iExec, anExec in enumerate(exOpts): - print(" [%d] %s" % (iExec, anExec)) - print("") - intVal = int(input("Please select which novelWriter executable to use: ")) - print("") - - if intVal >= 0 and intVal < nOpts: - useExec = exOpts[intVal] - else: - print("Error: Invalid selection.") - sys.exit(1) - - print("Using executable: %s " % useExec) - print("") - - # Create and Install Launcher - # =========================== - - # Generate launcher - desktopData = readFile(os.path.join("setup", "data", "novelwriter.desktop")) - desktopData = desktopData.replace("Exec=novelwriter", f"Exec={useExec}") - writeFile("novelwriter.desktop", desktopData) - - # Remove old desktop icon - exCode = subprocess.call( - ["xdg-desktop-icon", "uninstall", "novelwriter.desktop"] - ) - - # Install application launcher - exCode = subprocess.call( - ["xdg-desktop-menu", "install", "--novendor", "novelwriter.desktop"] - ) - if exCode == 0: - print("Installed menu launcher file") - else: - print(f"Error {exCode}: Could not install menu launcher file") - - # Install MimeType - # ================ - - exCode = subprocess.call([ - "xdg-mime", "install", "setup/data/x-novelwriter-project.xml" - ]) - if exCode == 0: - print("Installed mimetype") - else: - print(f"Error {exCode}: Could not install mimetype") - - # Install Icons - # ============= - - iconRoot = "setup/data/hicolor" - sizeArr = ["16", "24", "32", "48", "64", "128", "256"] - - # App Icon - for aSize in sizeArr: - exCode = subprocess.call([ - "xdg-icon-resource", "install", "--novendor", "--noupdate", - "--context", "apps", "--size", aSize, - f"{iconRoot}/{aSize}x{aSize}/apps/novelwriter.png", - "novelwriter" - ]) - if exCode == 0: - print(f"Installed app icon size {aSize}") - else: - print(f"Error {exCode}: Could not install app icon size {aSize}") - - # Mimetype - for aSize in sizeArr: - exCode = subprocess.call([ - "xdg-icon-resource", "install", "--noupdate", - "--context", "mimetypes", "--size", aSize, - f"{iconRoot}/{aSize}x{aSize}/mimetypes/application-x-novelwriter-project.png", - "application-x-novelwriter-project" - ]) - if exCode == 0: - print(f"Installed mime icon size {aSize}") - else: - print(f"Error {exCode}: Could not install mime icon size {aSize}") - - # Update Cache - exCode = subprocess.call(["xdg-icon-resource", "forceupdate"]) - if exCode == 0: - print("Updated icon cache") - else: - print(f"Error {exCode}: Could not update icon cache") - - # Clean up - if os.path.isfile("./novelwriter.desktop"): - os.unlink("./novelwriter.desktop") - - print("") - print("Done!") - print("") - - return - - -## -# XDG Uninstallation (xdg-uninstall) -## - -def xdgUninstall(): - """Will attempt to uninstall icons and make a launcher. - """ - print("") - print("XDG Uninstall") - print("=============") - print("") - - # Application Menu Icon - exCode = subprocess.call( - ["xdg-desktop-menu", "uninstall", "novelwriter.desktop"] - ) - if exCode == 0: - print("Uninstalled menu launcher file") - else: - print(f"Error {exCode}: Could not uninstall menu launcher file") - - # Desktop Icon - # (No longer installed) - exCode = subprocess.call( - ["xdg-desktop-icon", "uninstall", "novelwriter.desktop"] - ) - if exCode == 0: - print("Uninstalled desktop launcher file") - else: - print(f"Error {exCode}: Could not uninstall desktop launcher file") - - # Also include no longer used sizes - sizeArr = ["16", "22", "24", "32", "48", "64", "96", "128", "256", "512"] - - # App Icons - for aSize in sizeArr: - exCode = subprocess.call([ - "xdg-icon-resource", "uninstall", "--noupdate", - "--context", "apps", "--size", aSize, "novelwriter" - ]) - if exCode == 0: - print(f"Uninstalled app icon size {aSize}") - else: - print(f"Error {exCode}: Could not uninstall app icon size {aSize}") - - # Mimetype - for aSize in sizeArr: - exCode = subprocess.call([ - "xdg-icon-resource", "uninstall", "--noupdate", - "--context", "mimetypes", "--size", aSize, - "application-x-novelwriter-project" - ]) - if exCode == 0: - print(f"Uninstalled mime icon size {aSize}") - else: - print(f"Error {exCode}: Could not uninstall mime icon size {aSize}") - - # Update Cache - exCode = subprocess.call(["xdg-icon-resource", "forceupdate"]) - if exCode == 0: - print("Updated icon cache") - else: - print(f"Error {exCode}: Could not update icon cache") - - print("") - print("Done!") - print("") - - return - - -## -# WIN Installation (win-install) -## - -def winInstall(): - """Will attempt to install icons and make a launcher for Windows. - """ - import winreg - try: - import win32com.client - except ImportError: - print( - "ERROR: Package 'pywin32' is missing on this system.\n" - " Please run 'setup.py pip' to automatically install\n" - " dependecies, or run 'pip install --user pywin32'." - ) - sys.exit(1) - - print("") - print("Windows Install") - print("===============") - print("") - - numVers, hexVers, _ = extractVersion() - nwTesting = not hexVers[-2] == "f" - wShell = win32com.client.Dispatch("WScript.Shell") - - if nwTesting: - linkName = "novelWriter Testing %s.lnk" % numVers - else: - linkName = "novelWriter %s.lnk" % numVers - - desktopDir = wShell.SpecialFolders("Desktop") - desktopIcon = os.path.join(desktopDir, linkName) - - startMenuDir = wShell.SpecialFolders("StartMenu") - startMenuProg = os.path.join(startMenuDir, "Programs", "novelWriter") - startMenuIcon = os.path.join(startMenuProg, linkName) - - pythonDir = os.path.dirname(sys.executable) - pythonExe = os.path.join(pythonDir, "pythonw.exe") - - targetDir = os.path.abspath(os.path.dirname(__file__)) - targetPy = os.path.join(targetDir, "novelWriter.pyw") - targetIcon = os.path.join(targetDir, "novelwriter", "assets", "icons", "novelwriter.ico") - - if not os.path.isfile(targetPy): - shutil.copy2(os.path.join(targetDir, "novelWriter.py"), targetPy) - - print("") - print("Collecting Info ...") - print("Desktop Folder: %s" % desktopDir) - print("Start Menu Folder: %s" % startMenuDir) - print("Python Executable: %s" % pythonExe) - print("Target Executable: %s" % targetPy) - print("Target Icon: %s" % targetIcon) - print("") - - print("Creating Links ...") - if os.path.isfile(desktopIcon): - os.unlink(desktopIcon) - print("Deleted: %s" % desktopIcon) - - if os.path.isdir(startMenuProg): - for oldIcon in os.listdir(startMenuProg): - oldPath = os.path.join(startMenuProg, oldIcon) - if not oldIcon.startswith("novelWriter"): - continue - - isTesting = oldIcon.startswith("novelWriter Testing") - if isTesting and nwTesting: - os.unlink(oldPath) - print("Deleted: %s" % oldPath) - if not isTesting and not nwTesting: - os.unlink(oldPath) - print("Deleted: %s" % oldPath) - - else: - os.mkdir(startMenuProg) - print("Created: %s" % startMenuProg) - - wShortcut = wShell.CreateShortCut(desktopIcon) - wShortcut.TargetPath = targetPy - wShortcut.WorkingDirectory = targetDir - wShortcut.IconLocation = targetIcon - wShortcut.WindowStyle = 1 - wShortcut.save() - print("Created: %s" % desktopIcon) - - wShortcut = wShell.CreateShortCut(startMenuIcon) - wShortcut.TargetPath = targetPy - wShortcut.WorkingDirectory = targetDir - wShortcut.IconLocation = targetIcon - wShortcut.WindowStyle = 1 - wShortcut.save() - print("Created: %s" % startMenuIcon) - - print("") - print("Creating registry keys ...") - - def setKey(kPath, kName, kVal): - winreg.CreateKey(winreg.HKEY_CURRENT_USER, kPath) - regKey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, kPath, 0, winreg.KEY_WRITE) - winreg.SetValueEx(regKey, kName, 0, winreg.REG_SZ, kVal) - winreg.CloseKey(regKey) - - mimeIcon = os.path.join( - targetDir, "novelwriter", "assets", "icons", "x-novelwriter-project.ico" - ) - mimeExec = '"%s" "%s" "%%1"' % (pythonExe, targetPy) - - try: - setKey(r"Software\Classes\.nwx\OpenWithProgids", "novelWriterProject.nwx", "") - setKey(r"Software\Classes\novelWriterProject.nwx", "", "novelWriter Project File") - setKey(r"Software\Classes\novelWriterProject.nwx\DefaultIcon", "", mimeIcon) - setKey(r"Software\Classes\novelWriterProject.nwx\shell\open\command", "", mimeExec) - setKey(r"Software\Classes\Applications\novelWriter.pyw\SupportedTypes", ".nwx", "") - except WindowsError: - print("ERROR: Failed to set registry keys.") - print("") - - print("") - print("Done!") - print("") - - return - - -## -# WIN Uninstallation (win-uninstall) -## - -def winUninstall(): - """Will attempt to uninstall icons previously installed. - """ - import winreg - try: - import win32com.client - except ImportError: - print( - "ERROR: Package 'pywin32' is missing on this system.\n" - " Please run 'pip install --user pywin32' to install it." - ) - sys.exit(1) - - print("") - print("Windows Uninstall") - print("=================") - print("") - - numVers, hexVers, _ = extractVersion() - nwTesting = not hexVers[-2] == "f" - wShell = win32com.client.Dispatch("WScript.Shell") - - if nwTesting: - linkName = "novelWriter Testing %s.lnk" % numVers - else: - linkName = "novelWriter %s.lnk" % numVers - - desktopDir = wShell.SpecialFolders("Desktop") - desktopIcon = os.path.join(desktopDir, linkName) - - startMenuDir = wShell.SpecialFolders("StartMenu") - startMenuProg = os.path.join(startMenuDir, "Programs", "novelWriter") - startMenuIcon = os.path.join(startMenuProg, linkName) - - print("") - print("Deleting Links ...") - if os.path.isfile(desktopIcon): - os.unlink(desktopIcon) - print("Deleted: %s" % desktopIcon) - else: - print("Not Found: %s" % desktopIcon) - - if os.path.isfile(startMenuIcon): - os.unlink(startMenuIcon) - print("Deleted: %s" % startMenuIcon) - else: - print("Not Found: %s" % startMenuIcon) - - if os.path.isdir(startMenuProg): - if not os.listdir(startMenuProg): - os.rmdir(startMenuProg) - print("Deleted: %s" % startMenuProg) - else: - print("Not Empty: %s" % startMenuProg) - - print("") - print("Removing registry keys ...") - - theKeys = [ - r"Software\Classes\novelWriterProject.nwx\shell\open\command", - r"Software\Classes\novelWriterProject.nwx\shell\open", - r"Software\Classes\novelWriterProject.nwx\shell", - r"Software\Classes\novelWriterProject.nwx\DefaultIcon", - r"Software\Classes\novelWriterProject.nwx", - r"Software\Classes\.nwx\OpenWithProgids", - r"Software\Classes\.nwx", - r"Software\Classes\Applications\novelWriter.pyw\SupportedTypes", - r"Software\Classes\Applications\novelWriter.pyw", - ] - - for aKey in theKeys: - try: - winreg.DeleteKey(winreg.HKEY_CURRENT_USER, aKey) - print("Deleted: HKEY_CURRENT_USER\\%s" % aKey) - except WindowsError: - print("Not Found: HKEY_CURRENT_USER\\%s" % aKey) - - print("") - print("Done!") - print("") - - 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 - 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 - - # Set Target OS - if "--target-linux" in sys.argv: - sys.argv.remove("--target-linux") - targetOS = OS_LINUX - elif "--target-darwin" in sys.argv: - sys.argv.remove("--target-darwin") - targetOS = OS_DARWIN - elif "--target-win" in sys.argv: - sys.argv.remove("--target-win") - targetOS = OS_WIN - else: - targetOS = hostOS - - # Sign package - if "--sign" in sys.argv: - sys.argv.remove("--sign") - doSign = True - else: - doSign = False - - # First build - if "--first" in sys.argv: - sys.argv.remove("--first") - isFirstBuild = True - else: - isFirstBuild = False - - # Build snapshot - if "--snapshot" in sys.argv: - sys.argv.remove("--snapshot") - isSnapshot = True - else: - isSnapshot = False - - helpMsg = [ - "", - "novelWriter Setup Tool", - "======================", - "", - "This tool provides setup and build commands for installing or distibuting", - "novelWriter as a package on Linux, Mac and Windows. The available options", - "are as follows:", - "", - "Some of the commands can be targeted towards a different OS than the host OS.", - "To target the command, add one of '--target-linux', '--target-darwin' or", - "'--target-win'.", - "", - "General:", - "", - " help Print the help message.", - " pip Install all package dependencies for novelWriter using pip.", - " version Print the novelWriter version.", - " build-clean Will attempt to delete 'build' and 'dist' folders.", - "", - "Additional Builds:", - "", - " manual Build the help documentation as PDF (requires LaTeX).", - " sample Build the sample project zip file and add it to assets.", - " qtlupdate Update translation files for internationalisation.", - " The files to be updated must be provided as arguments.", - " qtlrelease Build the language files for internationalisation.", - " clean-assets Delete assets built by manual, sample and qtlrelease.", - " gen-plist Generates an Info.plist for use in a MacOS Bundle", - "", - "Python Packaging:", - "", - " import-i18n Import updated i18n files from a zip file.", - " minimal-zip Creates a minimal zip file of the core application without", - " all the other source files. Defaults to tailor the zip file", - " for the current OS, but accepts a target OS flag to build", - " for another OS.", - " build-deb Build a .deb package for Debian and Ubuntu. Add --sign to ", - " sign package.", - " 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-win-exe Build a setup.exe file with Python embedded for Windows.", - " The package must be built from a minimal windows zip file.", - " build-appimage Build an AppImage. Argument --linux-tag defaults to", - " manylinux_2_28_x86_64, and --python-version to 3.11.", - "", - "System Install:", - "", - " install Deprecated. Use pip or build.", - " 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.", - " xdg-uninstall Remove the launcher and icons for the current system as", - " installed by the 'xdg-install' command.", - " win-install Install desktop icon, start menu icon, and registry entries", - " for file association with .nwx files for Windows systems.", - " win-uninstall Remove desktop icon, start menu icon, and registry keys,", - " for the current system. Note that it only removes icons for", - " the version number of the package the command is run from.", - "", - ] - - # General - # ======= - - if "help" in sys.argv: - sys.argv.remove("help") - print("\n".join(helpMsg)) - sys.exit(0) - - if "version" in sys.argv: - sys.argv.remove("version") - numVers, _, _ = extractVersion(beQuiet=True) - print(numVers, end=None) - sys.exit(0) - - if "pip" in sys.argv: - sys.argv.remove("pip") - installPackages(hostOS) - - if "build-clean" in sys.argv: - sys.argv.remove("build-clean") - cleanBuildDirs() - - # Additional Builds - # ================= - - if "manual" in sys.argv: - sys.argv.remove("manual") - buildPdfManual() - - if "qtlrelease" in sys.argv: - sys.argv.remove("qtlrelease") - buildQtI18n() - - if "qtlupdate" in sys.argv: - sys.argv.remove("qtlupdate") - buildQtI18nTS(sys.argv) - sys.exit(0) # Don't continue execution - - if "sample" in sys.argv: - sys.argv.remove("sample") - buildSampleZip() - - if "clean-assets" in sys.argv: - sys.argv.remove("clean-assets") - cleanBuiltAssets() - - if "gen-plist" in sys.argv: - sys.argv.remove("gen-plist") - genMacOSPlist() - - # Python Packaging - # ================ - - if "import-i18n" in sys.argv: - sys.argv.remove("import-i18n") - importI18nUpdates(sys.argv) - sys.exit(0) # Don't continue execution - - if "minimal-zip" in sys.argv: - sys.argv.remove("minimal-zip") - makeMinimalPackage(targetOS) - - if "build-deb" in sys.argv: - sys.argv.remove("build-deb") - if hostOS == OS_LINUX: - if doSign: - signKey = "D6A9F6B8F227CF7C6F6D1EE84DBBE4B734B0BD08" - else: - signKey = None - makeDebianPackage(signKey=signKey) - else: - print("ERROR: Command 'build-deb' can only be used on Linux") - sys.exit(1) - - if "build-ubuntu" in sys.argv: - sys.argv.remove("build-ubuntu") - if hostOS == OS_LINUX: - makeForLaunchpad(doSign=doSign, isFirst=isFirstBuild, isSnapshot=isSnapshot) - else: - print("ERROR: Command 'build-ubuntu' can only be used on Linux") - sys.exit(1) - - if "build-win-exe" in sys.argv: - sys.argv.remove("build-win-exe") - makeWindowsEmbedded(sys.argv) - sys.exit(0) # Don't continue execution - - if "build-appimage" in sys.argv: - sys.argv.remove("build-appimage") - if hostOS == OS_LINUX: - sys.argv = makeAppImage(sys.argv) # Build appimage and prune its args - else: - print("ERROR: Command 'build-appimage' can only be used on Linux") - sys.exit(1) - - # General Installers - # ================== - - if "xdg-install" in sys.argv: - sys.argv.remove("xdg-install") - if hostOS == OS_WIN: - print("ERROR: Command 'xdg-install' cannot be used on Windows") - sys.exit(1) - else: - xdgInstall() - - if "xdg-uninstall" in sys.argv: - sys.argv.remove("xdg-uninstall") - if hostOS == OS_WIN: - print("ERROR: Command 'xdg-uninstall' cannot be used on Windows") - sys.exit(1) - else: - xdgUninstall() - - if "win-install" in sys.argv: - sys.argv.remove("win-install") - if hostOS == OS_WIN: - winInstall() - else: - print("ERROR: Command 'win-install' can only be used on Windows") - sys.exit(1) - - if "win-uninstall" in sys.argv: - sys.argv.remove("win-uninstall") - if hostOS == OS_WIN: - winUninstall() - else: - print("ERROR: Command 'win-uninstall' can only be used on Windows") - sys.exit(1) - - # Actions - # ======= - - if len(sys.argv) <= 1: - # Nothing more to do - sys.exit(0) - - # Run the standard setup - import setuptools # noqa: F401 - import warnings # noqa: F401 - warnings.warn( - "The setuptools section of setup.py is deprecated and will be removed " - "in a future release and the setup.py file will be renamed to avoid " - "conflicts with build tools. Please don't use the 'setup.py install' " - "command, and instead use the pip and build commands.", - DeprecationWarning - ) - setuptools.setup() - -# END Main +setuptools.setup() diff --git a/setup/README.md b/setup/README.md deleted file mode 100644 index 4aaa123f..00000000 --- a/setup/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Build and Install novelWriter - -The root folder of the repository contains a standard `setup.py` script. The `setup` folder -contains necessary files for the setup process, like icons and other files, and it also contains -additional scripts that are included in the minimal zip packages. - -## Setup Script - -The `setup.py` script in the root folder has all the necessary commands for generating packages, -installing icons, launcher, and file associations on Linux and Windows (no macOS support yet). - -In addition, it is a wrapper for `setuptools` and accept all commands associated with this tool. - -To list all custom package and install options in this script, run: -``` -./setup.py help -``` -## Windows Scripts - -The `windows_install.bat` and `windows_uninstall.bat` are provided as convenience scripts for -Windows users who aren't used to running commands from command line. They are both located in the -`setup` folder in the source package, but in the minimal zip package for Windows, they can be -found in the root folder. - -These scripts are designed to be double-clicked by the user, but can also be run from the command -line tool. In either case, they will check if Python is properly installed, and then run the -commands to install or uninstall the core dependencies of novelWriter, and then setup or remove the -desktop icon, start menu icon and registry entries. diff --git a/setup/macos/build.sh b/setup/macos/build.sh index b9192b42..2915730c 100755 --- a/setup/macos/build.sh +++ b/setup/macos/build.sh @@ -24,13 +24,13 @@ echo "Build Dir: $BUILD_DIR" pushd "$SRC_DIR" || exit 1 -VERSION="$(python3 setup.py version)" +VERSION="$(python3 pkgutils.py version)" echo "novelWriter Version: $VERSION" # --- Prepare Files ----------------------------------------------------------------------------- # echo "Generating Info.plist" -python3 setup.py gen-plist +python3 pkgutils.py gen-plist if [ -f $SRC_DIR/setup/macos/Info.plist ]; then echo "Found: setup/macos/Info.plist" else diff --git a/setup/make_clean.sh b/setup/make_clean.sh index 0f8cd300..61f9304f 100755 --- a/setup/make_clean.sh +++ b/setup/make_clean.sh @@ -1,10 +1,10 @@ #!/bin/bash set -e -if [ ! -f setup.py ]; then +if [ ! -f pkgutils.py ]; then echo "Must be called from the root folder of the source" exit 1 fi rm -rfv dist_upload -python3 setup.py build-clean +python3 pkgutils.py build-clean diff --git a/setup/make_pip.sh b/setup/make_pip.sh index f26e591e..3c1fdc54 100755 --- a/setup/make_pip.sh +++ b/setup/make_pip.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -if [ ! -f setup.py ]; then +if [ ! -f pkgutils.py ]; then echo "Must be called from the root folder of the source" exit 1 fi @@ -10,7 +10,7 @@ echo "" echo " Building Packages" echo "================================================================================" echo "" -python3 setup.py qtlrelease manual sample +python3 pkgutils.py qtlrelease manual sample python3 -m build mkdir -pv dist_upload cp -v dist/novelWriter-*.whl dist_upload/ diff --git a/setup/make_release.sh b/setup/make_release.sh index 98b142e6..bdbb2426 100755 --- a/setup/make_release.sh +++ b/setup/make_release.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -if [ ! -f setup.py ]; then +if [ ! -f pkgutils.py ]; then echo "Must be called from the root folder of the source" exit 1 fi @@ -10,20 +10,20 @@ echo "" echo " Building Dependencies" echo "================================================================================" echo "" -python3 setup.py clean-assets -python3 setup.py qtlrelease manual sample +python3 pkgutils.py clean-assets +python3 pkgutils.py qtlrelease manual sample echo "" echo " Building Minimal Packages" echo "================================================================================" echo "" -python3 setup.py minimal-zip --target-win -python3 setup.py minimal-zip --target-linux -python3 setup.py minimal-zip --target-darwin +python3 pkgutils.py minimal-zip --target-win +python3 pkgutils.py minimal-zip --target-linux +python3 pkgutils.py minimal-zip --target-darwin echo "" echo " Building Linux Packages" echo "================================================================================" echo "" -python3 setup.py build-deb --sign -python3 setup.py build-ubuntu --sign --first +python3 pkgutils.py build-deb --sign +python3 pkgutils.py build-ubuntu --sign --first diff --git a/setup/make_snapshot.sh b/setup/make_snapshot.sh index 371996a0..8ad797be 100755 --- a/setup/make_snapshot.sh +++ b/setup/make_snapshot.sh @@ -1,7 +1,7 @@ #!/bin/bash set -e -if [ ! -f setup.py ]; then +if [ ! -f pkgutils.py ]; then echo "Must be called from the root folder of the source" exit 1 fi @@ -10,11 +10,11 @@ echo "" echo " Building Dependencies" echo "================================================================================" echo "" -python3 setup.py clean-assets -python3 setup.py qtlrelease manual sample +python3 pkgutils.py clean-assets +python3 pkgutils.py qtlrelease manual sample echo "" echo " Building Linux Snapshots" echo "================================================================================" echo "" -python3 setup.py build-ubuntu --sign --snapshot +python3 pkgutils.py build-ubuntu --sign --snapshot diff --git a/setup/windows_install.bat b/setup/windows_install.bat index 4b2c80d6..b27d6640 100644 --- a/setup/windows_install.bat +++ b/setup/windows_install.bat @@ -7,12 +7,12 @@ python --version 2>NUL if errorlevel 1 goto errorNoPython echo Python found. OK. -if exist setup.py ( - echo setup.py found. OK. +if exist pkgutils.py ( + echo pkgutils.py found. OK. ) else ( cd .. - if exist setup.py ( - echo setup.py found. OK. + if exist pkgutils.py ( + echo pkgutils.py found. OK. ) else ( goto errorNoSetup ) @@ -23,7 +23,7 @@ echo. pip install --user pywin32 -r requirements.txt :: Create the desktop and start menu icons -python setup.py win-install +python pkgutils.py win-install pause goto:eof @@ -41,9 +41,9 @@ goto:eof :errorNoSetup echo. -echo ERROR^: Could not find the setup.py script. +echo ERROR^: Could not find the pkgutils.py script. echo. -echo Make sure you run setup.py from the novelWriter root folder. +echo Make sure you run pkgutils.py from the novelWriter root folder. echo. pause diff --git a/setup/windows_uninstall.bat b/setup/windows_uninstall.bat index 18523176..d96b7863 100644 --- a/setup/windows_uninstall.bat +++ b/setup/windows_uninstall.bat @@ -7,12 +7,12 @@ python --version 2>NUL if errorlevel 1 goto errorNoPython echo Python found. OK. -if exist setup.py ( - echo setup.py found. OK. +if exist pkgutils.py ( + echo pkgutils.py found. OK. ) else ( cd .. - if exist setup.py ( - echo setup.py found. OK. + if exist pkgutils.py ( + echo pkgutils.py found. OK. ) else ( goto errorNoSetup ) @@ -27,7 +27,7 @@ goto afterUninst :doUninst :: Remove the desktop and start menu icons -python setup.py win-uninstall +python pkgutils.py win-uninstall :: Remove the PyQt5 and pyenchant dependencies pip uninstall --yes -r requirements.txt @@ -51,9 +51,9 @@ goto:eof :errorNoSetup echo. -echo ERROR^: Could not find the setup.py script. +echo ERROR^: Could not find the pkgutils.py script. echo. -echo Make sure you run setup.py from the novelWriter root folder. +echo Make sure you run pkgutils.py from the novelWriter root folder. echo. pause