diff --git a/.gitignore b/.gitignore index 57dc50a9..830a409a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /deploy/ *.spec *.egg-info +setup.iss # Documentation /docs/build/ diff --git a/README.md b/README.md index 3b7835ed..ce5cfdf0 100644 --- a/README.md +++ b/README.md @@ -52,63 +52,65 @@ in principle work fine on other operating systems as well as long as dependencie tests are run on the latest versions of Ubuntu Linux, Windows Server and macOS. -## Installing and Running +# Installing and Running -You can runt novelWriter either from a downloaded copy of the source code, or by running: +novelWriter is available on [pypi.org](https://pypi.org/project/novelWriter/), and can be installed with: ```bash pip install novelwriter ``` -**Note:** On some systems you must use `pip3` instead for the Python 3 version. -You can update novelWriter to the latest version by running: +To upgrade an existing installation, use: ```bash pip install --upgrade novelwriter ``` -The application can then be started with one of the commands, depending on your Python configuration: +Dependencies are installed automatically, but can generally be installed with: +```bash +pip install -r requirements.txt +``` + +Below are some brief instructions on how to get started on different operating systems. + + +## Linux + +Either download the source, or install with pip. + +If you run from source, install the dependencies via pip, or directly from the OS repo. +There are very few dependencies, and they should be available in the standard repo. +The Python packages needed are `pyqt5`, `lxml` and `pyenchant`. + +### Installing from Source + +You can also install novelWriter from source with: +```bash +python3 setup.py sample +sudo python3 setup.py install +sudo python3 setup.py launcher +``` + +The last line will install the application icons and set up a launcher for novelWriter. +The method uses hardcoded paths, so it may or may not work for your Linux distro. +If you have any issues, please submit a ticket so the script can be tuned. + +The script may prompt you to choose which executable to configure if it finds more than one. + +### Running from Source + +If you want to run directly from the source, the application can be started with: ```bash ./novelWriter.py -python novelWriter.py -python3 novelWriter.py ``` -It also takes a few parameters for debugging and such, which can be listed with the switch `--help`. -The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output -for debugging. - -You can also provide a path to a folder containing a novelWriter project as the last parameter. - - -### Launcher and Icons - -In the root setup folder there are icons and scripts and a template for setting up a launcher on -Gnome desktops. You may need to modify those scripts slightly, but as they are, they work on Debian -and Ubuntu. For other operating systems, please consult your operating system documentation for how -to make those. Feel free to submit more if you are able to make them. - - -## Package Dependencies - -It is recommended that novelWriter runs with Qt 5.10 or later, and requires Python 3.6 or later. -Minimum version of Qt is 5.2. - - -### Linux - -Generally, dependencies can be installed via `pip` with: +You can also create a launcher for running directly from source with: ```bash -pip3 install -r requirements.txt +sudo python3 setup.py launcher ``` -You can also install the packages from the distro's own package manager. -For the apt package manager on Debian/Ubuntu systems, the following Python3 packages are needed: - -* `python3-pyqt5` for the GUI -* `python3-lxml` for writing project files -* `python3-enchant` for better spell checking (optional) +For more install options, see [Build and Install novelWriter](setup/BUILD.md). -### macOS +## macOS These instructions assume you're using brew, and have Python and pip set up. If not, see the [brew docs](https://docs.brew.sh/Homebrew-and-Python) for help. @@ -126,11 +128,16 @@ It comes with a lot of default dictionaries. brew install enchant ``` - ### Windows -On Windows, the `pip install` command is generally sufficient to install everything you need. -That should also install the Qt libraries and the spell check dictionary dependencies. +On Windows, you may first need to install Python. +See the [python.org](https://www.python.org/) website for download packages. +It is recommended that you install the latest version of Python 3.8. + +To install dependencies, run: +```bash +pip install --user -r requirements.txt +``` **Note:** On Windows, make sure Python3 is in your PATH if you want to launch novelWriter from command line. You can also right click the `novelWriter.py` file, create a shortcut, then right @@ -142,8 +149,11 @@ It should look something like this: C:\...\AppData\Local\Programs\Python\Python38\python.exe novelWriter.py ``` +You can also run the `make.py` script to generate a single executable, or an installer. +See [Build and Install novelWriter](setup/BUILD.md) for more details. -### Package Versions + +## Package Versions Exporting to Markdown requires PyQt/Qt 5.14. There are no known minimum for `lxml`, but the code was originally written with 4.2. The optional spell check library must be at least 3.0.0 to work @@ -155,8 +165,15 @@ checker, but more can be added to the `nw/assets/dict` folder. See the [README]( file in that folder for how to generate more dictionaries. Note that the difflib-based option is both slow and limited. +## Debugging -## Key Features +If you need to debug novelWriter, you must run it from command line. +It takes a few parameters, which can be listed with the switch `--help`. +The `--info`, `--debug` or `--verbose` flags are particularly useful for increasing logging output +for debugging. + + +# Key Features Some features of novelWriter are listed below. Consult the documentation for more information. diff --git a/docs/source/conf.py b/docs/source/conf.py index b72b8db3..a76fce4c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -12,7 +12,6 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os # import sys # sys.path.insert(0, os.path.abspath(".")) import os diff --git a/install.py b/install.py deleted file mode 100755 index 11754a26..00000000 --- a/install.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - -import os -import sys -import getopt -import subprocess - -# Defaults -buildWindowed = True - -# Parse Options -shortOpt = "hd" -longOpt = [ - "help", - "debug", -] -helpMsg = ( - "\n" - "novelWriter Install Script\n" - "\n" - "Usage:\n" - " -h, --help Print this message.\n" - " -d, --debug Build novelWriter for debugging. To debug novelwriter after build,\n" - " run it from command line with the debug options. Please check the\n" - " novelWriter --help output for details.\n" -) - -try: - inOpts, inArgs = getopt.getopt(sys.argv[1:], shortOpt, longOpt) -except getopt.GetoptError: - print(helpMsg) - sys.exit(2) - -for inOpt, inArg in inOpts: - if inOpt in ("-h", "--help"): - print(helpMsg) - sys.exit(0) - elif inOpt in ("-d", "--debug"): - buildWindowed = False - -# Run pip -packList = ["pyinstaller"] -with open("requirements.txt", mode="r") as reqFile: - for reqPack in reqFile: - if len(reqPack.strip()) > 0: - packList.append(reqPack) - -for packName in packList: - print("Installing package dependency: %s" % packName) - try: - subprocess.call([sys.executable, "-m", "pip", "install", packName]) - except Exception as e: - print("Failed with error:") - print(str(e)) - -# Run pyinstaller -if sys.platform.startswith("win32"): - dotDot = ";" -else: - dotDot = ":" - -instOpt = [ - "--name=novelWriter", - "--clean", - "--onefile", - "--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"), - "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), -] -if buildWindowed: - instOpt.append("--windowed") - -instOpt.append("novelWriter.py") - -import PyInstaller.__main__ # noqa: E402 -PyInstaller.__main__.run(instOpt) - -print("") -print("##################") -print(" Build Finished") -print("##################") -print("") -print("If everything went well, the novelWriter executable should be in the folder named 'dist'") -print("") diff --git a/make.py b/make.py new file mode 100755 index 00000000..15ed18ae --- /dev/null +++ b/make.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +This script will either build: + * A single file executable named dist/novelWriter.exe. This is a quite + slow option, and the file is fairly big. Option --onefile + * A single directory named dist/novelWriter with a novelWriter.exe, and + all dependecies included. This is the default. + * The latter can be combined with a build stage of a setup.exe file + named setup-novelwriter-.exe. Option --setup. + +In addition, providing the --pip flag will cause the script to try to +install all dependencies needed for runing the build, and for running +novelWriter itself. +""" + +import os +import sys +import shutil +import subprocess + +OS_NONE = 0 +OS_LINUX = 1 +OS_WIN = 2 +OS_DARWIN = 3 + +# =============================================================================================== # +# Package Installer +# =============================================================================================== # + +def installPackages(hostOS): + """Install package dependencies both for this script and for running + novelWriter itself. + """ + print("") + print("Installing Dependencies") + print("#######################") + print("") + + installQueue = ["pip", "pyinstaller", "-r requirements.txt"] + if hostOS == OS_DARWIN: + installQueue.append("pyobjc") + + pyCmd = [sys.executable, "-m"] + pipCmd = ["pip", "install", "--user", "--upgrade"] + for stepCmd in installQueue: + pkgCmd = stepCmd.split(" ") + try: + subprocess.call(pyCmd + pipCmd + pkgCmd) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + + return + +# =============================================================================================== # +# Run PyInstaller on Package +# =============================================================================================== # + +def freezePackage(buildWindowed, oneFile, makeSetup, hostOS): + """Run PyInstaller to freeze the packages. This assumes all + dependencies are already in place. + """ + import PyInstaller.__main__ # noqa: E402 + + print("") + print("Running PyInstaller") + print("###################") + print("") + + if hostOS == OS_WIN: + dotDot = ";" + else: + dotDot = ":" + + sys.modules["FixTk"] = None + instOpt = [ + "--name=novelWriter", + "--clean", + "--add-data=%s%s%s" % (os.path.join("nw", "assets"), dotDot, "assets"), + "--icon=%s" % os.path.join("nw", "assets", "icons", "novelwriter.ico"), + "--exclude-module=PyQt5.QtQml", + "--exclude-module=PyQt5.QtBluetooth", + "--exclude-module=PyQt5.QtDBus", + "--exclude-module=PyQt5.QtMultimedia", + "--exclude-module=PyQt5.QtMultimediaWidgets", + "--exclude-module=PyQt5.QtNetwork", + "--exclude-module=PyQt5.QtNetworkAuth", + "--exclude-module=PyQt5.QtNfc", + "--exclude-module=PyQt5.QtQuick", + "--exclude-module=PyQt5.QtQuickWidgets", + "--exclude-module=PyQt5.QtRemoteObjects", + "--exclude-module=PyQt5.QtSensors", + "--exclude-module=PyQt5.QtSerialPort", + "--exclude-module=PyQt5.QtSql", + "--exclude-module=FixTk", + "--exclude-module=tcl", + "--exclude-module=tk", + "--exclude-module=_tkinter", + "--exclude-module=tkinter", + "--exclude-module=Tkinter", + ] + + if buildWindowed: + instOpt.append("--windowed") + + if oneFile and not makeSetup: + instOpt.append("--onefile") + else: + instOpt.append("--onedir") + + instOpt.append("novelWriter.py") + + # Make sample.zip first + try: + subprocess.call([sys.executable, "setup.py", "sample"]) + except Exception as e: + print("Failed with error:") + print(str(e)) + sys.exit(1) + + PyInstaller.__main__.run(instOpt) + + if not oneFile: + # These files are not needed, and take up a fair bit of space. + delFiles = [] + if hostOS == OS_WIN: + delFiles = [ + "Qt5DBus.dll", + "Qt5Network.dll", + "Qt5Qml.dll", + "Qt5QmlModels.dll", + "Qt5Quick.dll", + "Qt5Quick3D.dll", + "Qt5Quick3DAssetImport.dll", + "Qt5Quick3DRender.dll", + "Qt5Quick3DRuntimeRender.dll", + "Qt5Quick3DUtils.dll", + "Qt5Sql.dll" + ] + elif hostOS == OS_LINUX: + delFiles = [ + "libQt5DBus.so.5", + "libQt5Network.so.5", + "libQt5Qml.so.5", + "libQt5QmlModels.so.5", + "libQt5Quick.so.5", + "libQt5Quick3D.so.5", + "libQt5Quick3DAssetImport.so.5", + "libQt5Quick3DRender.so.5", + "libQt5Quick3DRuntimeRender.so.5", + "libQt5Quick3DUtils.so.5", + "libQt5Sql.so.5" + ] + distDir = os.path.join(os.getcwd(), "dist", "novelWriter") + for delFile in delFiles: + delPath = os.path.join(distDir, delFile) + if os.path.isfile(delPath): + print("Deleting file: %s" % delPath) + os.unlink(delPath) + + print("") + print("Build Finished") + print("") + print("The novelWriter executable should be in the folder named 'dist'") + print("") + + return + +# =============================================================================================== # +# Inno Setup Builder +# =============================================================================================== # + +def innoSetup(): + """Run the Inno Setup tool to build a setup.exe file for Windows. + """ + print("") + print("Running Inno Setup") + print("##################") + print("") + + # Read the iss template + issData = "" + with open(os.path.join("setup", "win_setup.iss"), mode="r") as inFile: + issData = inFile.read() + + import nw # noqa: E402 + issData = issData.replace(r"%%version%%", nw.__version__) + issData = issData.replace(r"%%dir%%", os.getcwd()) + + with open("setup.iss", mode="w+") as outFile: + outFile.write(issData) + + try: + subprocess.call(["iscc", "setup.iss"]) + except Exception as e: + print("Inno Setup failed with error:") + print(str(e)) + sys.exit(1) + + return + +# =============================================================================================== # +# Clean Build and Dist Folders +# =============================================================================================== # + +def cleanInstall(): + """Recursively delete the 'build' and 'dist' folders. + """ + print("") + print("Cleaning up build environment ...") + + buildDir = os.path.join(os.getcwd(), "build") + if os.path.isdir(buildDir): + try: + shutil.rmtree(buildDir) + print("Deleted folder 'build'") + except Exception as e: + print("Error: Cannot delete 'build' folder.") + print(str(e)) + sys.exit(1) + else: + print("Folder 'build' not found") + + distDir = os.path.join(os.getcwd(), "dist") + if os.path.isdir(distDir): + try: + shutil.rmtree(distDir) + print("Deleted folder 'dist'") + except Exception as e: + print("Error: Cannot delete 'dist' folder.") + print(str(e)) + sys.exit(1) + else: + print("Folder 'dist' not found") + + print("") + + return + +# =============================================================================================== # +# Process Build Steps +# =============================================================================================== # + +if __name__ == "__main__": + """Parse command line options and run the commands. + """ + # Detect OS + if sys.platform.startswith("linux"): + hostOS = OS_LINUX + elif sys.platform.startswith("darwin"): + hostOS = OS_DARWIN + elif sys.platform.startswith("win32"): + hostOS = OS_WIN + elif sys.platform.startswith("cygwin"): + hostOS = OS_WIN + else: + hostOS = OS_NONE + + # Flags and Variables + buildWindowed = True + oneFile = False + makeSetup = False + doFreeze = False + + helpMsg = ( + "\n" + "novelWriter Make Tool\n" + "=====================\n" + "This tool provides build commands for distibuting novelWriter as a\n" + "package. The available options are as follows:\n" + "\n" + "help Print the help message.\n" + "freeze Freeze the package and produces a folder of all\n" + " dependencies using pyinstaller.\n" + "onefile Build a standalone executable with all dependencies\n" + " bundled. Implies 'freeze', cannot be used with 'setup'.\n" + "pip Run pip to install all package dependencies for\n" + " novelWriter and this build tool.\n" + "setup Build a setup.exe installer for Windows. This option\n" + " automaticall disables the 'onefile' option.\n" + "clean This will attempt to delete the 'build' and 'dist'\n" + " folders in the current folder.\n" + ) + + if "help" in sys.argv or len(sys.argv) <= 1: + print(helpMsg) + sys.exit(0) + + if not os.path.isfile(os.path.join(os.getcwd(), "novelWriter.py")): + print("Error: This script must be run in the root folder of novelWriter.") + sys.exit(1) + + if not os.path.isdir(os.path.join(os.getcwd(), "nw")): + print("Error: This script must be run in the root folder of novelWriter.") + sys.exit(1) + + if "clean" in sys.argv: + sys.argv.remove("clean") + cleanInstall() + + if "pip" in sys.argv: + sys.argv.remove("pip") + installPackages(hostOS) + + if "freeze" in sys.argv: + sys.argv.remove("freeze") + doFreeze = True + + if "onefile" in sys.argv: + sys.argv.remove("onefile") + doFreeze = True + oneFile = True + + if "setup" in sys.argv: + sys.argv.remove("setup") + if hostOS == OS_WIN: + oneFile = False + makeSetup = True + else: + print("Error: Argument 'setup' for Inno Setup is Windows only.") + sys.exit(1) + + if doFreeze: + freezePackage(buildWindowed, oneFile, makeSetup, hostOS) + + if makeSetup: + innoSetup() + +# END Main diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..9787c3bd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index ff0117c5..ad1c6ddb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,49 @@ [metadata] -license_files = LICENSE.md +name = novelWriter version = attr: nw.__version__ +author = Veronica Berglyd Olsen +author_email = code@vkbo.net +description = A markdown-like document editor for writing novels +url = https://novelwriter.io +long_description = file: README.md +long_description_content_type = text/markdown +license_file = LICENSE.md +license = GNU General Public License v3 +classifiers = + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: Implementation :: CPython + License :: OSI Approved :: GNU General Public License v3 (GPLv3) + Development Status :: 4 - Beta + Operating System :: OS Independent + Intended Audience :: End Users/Desktop + Natural Language :: English + Topic :: Text Editors +python_requires = >=3.6 +install_requires = + pyqt5>=5.2.1 + lxml>=4.2.0 + pyenchant>=3.0.0 +project_urls = + Bug Tracker = https://github.com/vkbo/novelWriter/issues + Documentation = https://github.com/vkbo/novelWriter/issues + Source Code = https://github.com/vkbo/novelWriter + +[options] +include_package_data = True +packages = find: + +[options.packages.find] +exclude = docs, tests, sample + +[options.entry_points] +console_script = + novelWriter-cli = nw:main +gui_scripts = + novelWriter = nw:main [bdist_wheel] universal = 0 diff --git a/setup.py b/setup.py index 6b10a84e..4683e3f2 100755 --- a/setup.py +++ b/setup.py @@ -1,30 +1,26 @@ #!/usr/bin/env python3 import os import sys +import shutil import subprocess import setuptools -## -# Build the Package -## +# =============================================================================================== # +# Qt Assistant Documentation Builder +# =============================================================================================== # -buildDocs = False -buildSample = False +def buildQtDocs(): + """This function will build the documentation as a Qt help file. The + file is then copied into the nw/assets/help directory and can be + included in builds. -if "qthelp" in sys.argv: - buildDocs = True - sys.argv.remove("qthelp") - -if "sample" in sys.argv: - buildSample = True - sys.argv.remove("sample") - -## -# Qt Assistant Documentation -## - -if buildDocs: + Depends on packages: + * pip install sphinx + * pip install sphinx-rtd-theme + * pip install sphinxcontrib-qthelp + It also requires the qhelpgenerator to be available on the system. + """ buildDir = os.path.join("docs", "build", "qthelp") helpDir = os.path.join("nw", "assets", "help") @@ -41,14 +37,14 @@ if buildDocs: try: subprocess.call(["make", "-C", "docs", "qthelp"]) except Exception as e: - print("Failed with error:") + print("QtHelp Build Error:") print(str(e)) buildFail = True try: subprocess.call(["qhelpgenerator", os.path.join(buildDir, inFile)]) except Exception as e: - print("Failed with error:") + print("QtHelp Build Error:") print(str(e)) buildFail = True @@ -56,7 +52,7 @@ if buildDocs: try: os.mkdir(helpDir) except Exception as e: - print("Failed with error:") + print("QtHelp Build Error:") print(str(e)) buildFail = True @@ -68,7 +64,7 @@ if buildDocs: os.rename(os.path.join(buildDir, outFile), os.path.join(helpDir, outFile)) os.rename(os.path.join(buildDir, datFile), os.path.join(helpDir, datFile)) except Exception as e: - print("Failed with error:") + print("QtHelp Build Error:") print(str(e)) buildFail = True @@ -80,11 +76,20 @@ if buildDocs: print("Documentation build: OK") print("") -## -# Sample Project ZIP file -## + return -if buildSample: +# =============================================================================================== # +# Sample Project ZIP File Builder +# =============================================================================================== # + +def buildSampleZip(): + """Bundle the sample project into a single zip file to be saved into + the nw/assets folder for further bundling into builds. + """ + print("") + print("Building Sample ZIP File") + print("========================") + print("") srcSample = "sample" dstSample = os.path.join("nw", "assets", "sample.zip") @@ -96,8 +101,10 @@ if buildSample: 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) @@ -105,58 +112,197 @@ if buildSample: print("Error: Could not find sample project source directory.") sys.exit(1) -if len(sys.argv) == 1: - # Nothing more to do - sys.exit(0) + print("") + print("Built file: %s" % dstSample) + print("") -## -# Build the Package -## + return -# Read content from files -with open("README.md", "r") as inFile: - longDescription = inFile.read() +# =============================================================================================== # +# Create Launcher +# =============================================================================================== # -setuptools.setup( - name = "novelWriter", - # version = __version__, # Set in setup.cfg - author = "Veronica Berglyd Olsen", - author_email = "code@vkbo.net", - description = "A markdown-like document editor for writing novels", - long_description = longDescription, - long_description_content_type = "text/markdown", - license = "GNU General Public License v3", - url = "https://novelwriter.io", - entry_points = { - "console_scripts" : ["novelWriter-cli=nw:main"], - "gui_scripts" : ["novelWriter=nw:main"], - }, - packages = setuptools.find_packages(exclude=["docs", "tests", "sample"]), - include_package_data = True, - package_data = {"": ["*.conf"]}, - project_urls = { - "Bug Tracker": "https://github.com/vkbo/novelWriter/issues", - "Documentation": "https://github.com/vkbo/novelWriter/issues", - "Source Code": "https://github.com/vkbo/novelWriter", - }, - classifiers = [ - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: Implementation :: CPython", - "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", - "Development Status :: 4 - Beta", - "Operating System :: OS Independent", - "Intended Audience :: End Users/Desktop", - "Natural Language :: English", - "Topic :: Text Editors", - ], - python_requires = ">=3.6", - install_requires = [ - "pyqt5>=5.2.1", - "lxml>=4.2.0", - "pyenchant>=3.0.0", - ], -) +def makeLauncherLinux(): + """Will attempt to install icons and make a launcher. + """ + print("") + print("Creating Launcher") + print("=================") + print("") + + 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) + + # Read the Template + desktopData = "" + with open(os.path.join("setup", "novelwriter.desktop"), mode="r") as inFile: + desktopData = inFile.read() + + desktopData = desktopData.replace(r"%%exec%%", useExec) + + desktopFile = "/usr/share/applications/novelwriter.desktop" + try: + with open(desktopFile, mode="w+") as outFile: + outFile.write(desktopData) + print("Wrote file: %s" % desktopFile) + except Exception as e: + print("Error: Could not write novelwriter.desktop file.") + print(str(e)) + sys.exit(1) + + print("") + + # Copy Icons + + iconDirs = [ + "/usr/share/icons/hicolor/24x24/apps", + "/usr/share/icons/hicolor/48x48/apps", + "/usr/share/icons/hicolor/96x96/apps", + "/usr/share/icons/hicolor/256x256/apps", + "/usr/share/icons/hicolor/512x512/apps", + "/usr/share/icons/hicolor/scalable/apps", + "/usr/share/icons/hicolor/scalable/mimetypes", + ] + for iconDir in iconDirs: + if not os.path.isdir: + try: + os.mkdir(iconDir) + print("Created folder: %s" % iconDir) + except Exception as e: + print("Error: Could not make folder: %s" % iconDir) + print(str(e)) + + copyList = [( + "setup/icons/24x24/novelwriter.png", + "/usr/share/icons/hicolor/24x24/apps/novelwriter.png" + ), ( + "setup/icons/48x48/novelwriter.png", + "/usr/share/icons/hicolor/48x48/apps/novelwriter.png" + ), ( + "setup/icons/96x96/novelwriter.png", + "/usr/share/icons/hicolor/96x96/apps/novelwriter.png" + ), ( + "setup/icons/256x256/novelwriter.png", + "/usr/share/icons/hicolor/256x256/apps/novelwriter.png" + ), ( + "setup/icons/512x512/novelwriter.png", + "/usr/share/icons/hicolor/512x512/apps/novelwriter.png" + ), ( + "setup/icons/novelwriter.svg", + "/usr/share/icons/hicolor/scalable/apps/novelwriter.svg" + ), ( + "setup/icons/x-novelwriter-project.svg", + "/usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg" + ), ( + "setup/mime/x-novelwriter-project.xml", + "/usr/share/mime/packages/x-novelwriter-project.xml" + )] + for srcFile, dstFile in copyList: + try: + shutil.copyfile(srcFile, dstFile) + print("Copied file to: %s" % dstFile) + except Exception as e: + print("Error: Could not copy file: %s" % srcFile) + print(str(e)) + + print("") + + # Update System + try: + subprocess.call(["update-mime-database", "/usr/share/mime/"]) + print("Updated mime database.") + except Exception as e: + print("Error: Filed to update mime database.") + print(str(e)) + + try: + subprocess.call(["update-icon-caches", "/usr/share/icons/*"]) + print("Updated icon cache.") + except Exception as e: + print("Error: Filed to update icon cache.") + print(str(e)) + + print("") + print("Done!") + print("") + + return + +# =============================================================================================== # +# Process Jobs +# =============================================================================================== # + +if __name__ == "__main__": + + helpMsg = ( + "\n" + "novelWriter Setup Tool\n" + "======================\n" + "This tool provides some additional setup commands for novelWriter.\n" + "\n" + "help Print the help message.\n" + "gthelp Build the help documentation for use with the QtAssistant.\n" + "sample Build the sample project as a zip file.\n" + "launcher Install launcher icons for freedesktop systems.\n" + ) + + if "help" in sys.argv: + sys.argv.remove("help") + print(helpMsg) + sys.exit(0) + + if "qthelp" in sys.argv: + sys.argv.remove("qthelp") + buildQtDocs() + + if "sample" in sys.argv: + sys.argv.remove("sample") + buildSampleZip() + + if "launcher" in sys.argv: + sys.argv.remove("launcher") + makeLauncherLinux() + + if len(sys.argv) <= 1: + # Nothing more to do + sys.exit(0) + + # Run the standard setup + setuptools.setup() + +# END Main diff --git a/setup/BUILD.md b/setup/BUILD.md new file mode 100644 index 00000000..81479cf7 --- /dev/null +++ b/setup/BUILD.md @@ -0,0 +1,47 @@ +# Build and Install novelWriter + +The root folder of the repository contains two scripts for setup and install: + + +## Script `setup.py` + +The `setup.py` is a standard Python setup script with a couple of additional options: + +* `qthelp`: Will attempt to build a single file QtAssistand documentation file. + This requires the Qt tools to be installed on the local system, as well as the sphinx build tools + for the documentation. +* `sample`: Will create a `sample.zip` file in the `nw/assets` folder. + This is the file the New Project Wizard uses to generate an example project. + If novelWriter is run from source, this file is not needed. +* `launcher`: Will try to copy the novelWriter icons and create a novelWriter.desktop file to launch + the application. This should work on standard Linux desktops. + +To install novelWriter as a local Python package, run: +```bash +sudo python setup.py install +``` + +## Script `make.py` + +The `make.py` script provides a number of convenient options for building packages if novelWriter. + +Usage: +```bash +python make.py [command] +``` + +It currently accept the following commands: + +* `help`: Print the help message. +* `freeze`: Freeze the package and produces a folder of all dependencies using pyinstaller. +* `onefile`: Build a standalone executable with all dependencies bundled. + Implies `freeze`, cannot be used with `setup`. +* `pip`: Run pip to install all package dependencies for novelWriter and this build tool. +* `setup`: Build a setup.exe installer for Windows. + This option automaticall disables the `onefile` option. +* `clean`: This will attempt to delete the `build` and `dist` folders in the current folder. + +For instance, to create a Windows installer, run: +```bash +python make.py freeze setup +``` diff --git a/setup/installDebianUbuntu.sh b/setup/installDebianUbuntu.sh deleted file mode 100755 index c47f4d26..00000000 --- a/setup/installDebianUbuntu.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash - -cd .. - -EXEC=$(pwd)/novelWriter.py -EXEC=$(echo $EXEC | sed 's_/_\\/_g') - -sed "s/%%exec%%/$EXEC/g" setup/novelwriter.desktop > /usr/share/applications/novelwriter.desktop - -if [ ! -d /usr/share/icons/hicolor/24x24/apps ]; then - mkdir -pv /usr/share/icons/hicolor/24x24/apps -fi -if [ ! -d /usr/share/icons/hicolor/48x48/apps ]; then - mkdir -pv /usr/share/icons/hicolor/48x48/apps -fi -if [ ! -d /usr/share/icons/hicolor/96x96/apps ]; then - mkdir -pv /usr/share/icons/hicolor/96x96/apps -fi -if [ ! -d /usr/share/icons/hicolor/256x256/apps ]; then - mkdir -pv /usr/share/icons/hicolor/256x256/apps -fi -if [ ! -d /usr/share/icons/hicolor/512x512/apps ]; then - mkdir -pv /usr/share/icons/hicolor/512x512/apps -fi -if [ ! -d /usr/share/icons/hicolor/scalable/apps ]; then - mkdir -pv /usr/share/icons/hicolor/scalable/apps -fi -if [ ! -d /usr/share/icons/hicolor/scalable/mimetypes ]; then - mkdir -pv /usr/share/icons/hicolor/scalable/mimetypes -fi - -cp -v setup/icons/24x24/novelwriter.png /usr/share/icons/hicolor/24x24/apps/ -cp -v setup/icons/48x48/novelwriter.png /usr/share/icons/hicolor/48x48/apps/ -cp -v setup/icons/96x96/novelwriter.png /usr/share/icons/hicolor/96x96/apps/ -cp -v setup/icons/256x256/novelwriter.png /usr/share/icons/hicolor/256x256/apps/ -cp -v setup/icons/512x512/novelwriter.png /usr/share/icons/hicolor/512x512/apps/ -cp -v setup/icons/novelwriter.svg /usr/share/icons/hicolor/scalable/apps/ -cp -v setup/icons/x-novelwriter-project.svg /usr/share/icons/hicolor/scalable/mimetypes/application-x-novelwriter-project.svg -cp -v setup/mime/x-novelwriter-project.xml /usr/share/mime/packages/ - -update-mime-database /usr/share/mime/ -update-icon-caches /usr/share/icons/* diff --git a/setup/win_setup.iss b/setup/win_setup.iss new file mode 100644 index 00000000..8b6be609 --- /dev/null +++ b/setup/win_setup.iss @@ -0,0 +1,52 @@ +; Script generated by the Inno Setup Script Wizard. +; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! + +#define nwAppDir "%%dir%%\dist" +#define nwAppName "novelWriter" +#define nwAppVersion "%%version%%" +#define nwAppPublisher "novelWriter" +#define nwAppURL "http://novelWriter.io" +#define nwAppExeName "novelWriter.exe" + +[Setup] +; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. +; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) +AppId={{459A75D0-951F-4932-9809-6002EC8E733E} +AppName={#nwAppName} +AppVersion={#nwAppVersion} +AppVerName={#nwAppName} {#nwAppVersion} +AppPublisher={#nwAppPublisher} +AppPublisherURL={#nwAppURL} +AppSupportURL={#nwAppURL} +AppUpdatesURL={#nwAppURL} +DefaultDirName={autopf}\{#nwAppName} +DisableProgramGroupPage=yes +; The [Icons] "quicklaunchicon" entry uses {userappdata} but its [Tasks] entry has a proper IsAdminInstallMode Check. +UsedUserAreasWarning=no +; Uncomment the following line to run in non administrative install mode (install for current user only.) +;PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +OutputDir={#nwAppDir} +OutputBaseFilename=novelwriter_{#nwAppVersion}_win_amd64_setup +Compression=lzma +SolidCompression=yes +WizardStyle=modern +ArchitecturesInstallIn64BitMode=x64 + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked; OnlyBelowVersion: 6.1; Check: not IsAdminInstallMode + +[Files] +Source: "{#nwAppDir}\novelWriter\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{autoprograms}\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}" +Name: "{autodesktop}\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"; Tasks: desktopicon +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#nwAppName}"; Filename: "{app}\{#nwAppExeName}"; Tasks: quicklaunchicon + +[Run] +Filename: "{app}\{#nwAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(nwAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent