Merge branch 'dev' into feature/build_gui
This commit is contained in:
@@ -42,7 +42,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10"]
|
||||
python-version: ["3.11"]
|
||||
linux-tag: ["manylinux_2_24_x86_64", "manylinux_2_28_x86_64"]
|
||||
steps:
|
||||
- name: Python Setup
|
||||
@@ -57,13 +57,6 @@ jobs:
|
||||
- name: Checkout Source
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get Version
|
||||
shell: bash
|
||||
run: |
|
||||
NW_VER=$(python setup.py version)
|
||||
echo "novelWriter Version: $NW_VER"
|
||||
echo "VERSION=$NW_VER" >> $GITHUB_ENV
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -76,27 +69,20 @@ jobs:
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: novelWriter-${{ env.VERSION }}-py${{ matrix.python-version }}-${{ matrix.linux-tag }}.AppImage
|
||||
path: dist_appimage/novelWriter-${{ env.VERSION }}-py${{ matrix.python-version }}-${{ matrix.linux-tag }}.AppImage
|
||||
name: novelWriter-Packages
|
||||
path: |
|
||||
dist_appimage/novelWriter*.AppImage
|
||||
dist_appimage/novelWriter*.AppImage.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
buildMac:
|
||||
needs: buildAssets
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Get Version
|
||||
shell: bash
|
||||
run: |
|
||||
NW_VER=$(python3 setup.py version)
|
||||
echo "novelWriter Version: $NW_VER"
|
||||
echo "VERSION=$NW_VER" >> $GITHUB_ENV
|
||||
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -106,17 +92,12 @@ jobs:
|
||||
- name: Build App Bundle
|
||||
run: ./setup/macos/build.sh
|
||||
|
||||
- name: Upload App Zip
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: novelWriter-${{ env.VERSION }}-macos.app.zip
|
||||
path: dist_macos/novelWriter-${{ env.VERSION }}-macos.app.zip
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload DMG
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: novelWriter-${{ env.VERSION }}-macos.dmg
|
||||
path: dist_macos/novelWriter-${{ env.VERSION }}-macos.dmg
|
||||
name: novelWriter-Packages
|
||||
path: |
|
||||
dist_macos/novelWriter*.dmg
|
||||
dist_macos/novelWriter*.dmg.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
+31
-17
@@ -17,6 +17,22 @@ to build the various components like the translation files and documentation.
|
||||
now deprecated, but many systems still have both Python 2 and 3. For such systems, the command
|
||||
``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:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python setup.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:
|
||||
|
||||
@@ -58,37 +74,35 @@ source, dependencies can still be installed from PyPi with:
|
||||
|
||||
.. _a_source_install:
|
||||
|
||||
Install from Source
|
||||
===================
|
||||
Build and Install from Source
|
||||
=============================
|
||||
|
||||
You can download the latest version of novelWriter from the source repository on GitHub_ and run
|
||||
the setup manually. It is equivalent to what the ``pip install`` command does, and it installs
|
||||
novelWriter to the default location for Python packages.
|
||||
|
||||
This step requires that you have ``setuptools`` installed on your system. If you don't have it
|
||||
installed, it can usually be installed from your distro's repository. For Debian and Ubuntu this is
|
||||
achieved with:
|
||||
If you want to install novelWriter directly from the source available on GitHub_, you must first
|
||||
build the package using the Python Packaging Authority's build tool. It can be installed with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt install python3-setuptools
|
||||
pip install --upgrade build
|
||||
|
||||
The package is also available from PyPi:
|
||||
On Debian-based systems the tool can also be installed with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install --user setuptools
|
||||
sudo apt install python3-build
|
||||
|
||||
With ``setuptools`` in place, novelWriter can be installed to the user space with:
|
||||
With the tool installed, run the following command from the root of the novelWriter source code:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python setup.py install --user
|
||||
python -m build
|
||||
|
||||
.. tip::
|
||||
This should generate two files in the ``dist/`` folder at your current location. One with file
|
||||
extension ``.tar.gz`` and one with extension ``.whl``. The latter is the package you want to
|
||||
install, here with example version number 2.0.7, but your may be different:
|
||||
|
||||
The main setup script has a number of options that may be useful to you. You can list them by
|
||||
running ``python setup.py help``.
|
||||
.. code-block:: bash
|
||||
|
||||
pip install --user dist/novelWriter-2.0.7-py3-none-any.whl
|
||||
|
||||
|
||||
.. _a_source_i18n:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
# PyQt6 Linguist Tool
|
||||
|
||||
This is a copy of the `pylupdate` tool from PyQt6 version 6.2.1 from
|
||||
[PyPi](https://pypi.org/project/PyQt6/).
|
||||
The tool is stripped down to run for novelWriter.
|
||||
|
||||
PyQt6 is licensed under GPL 3.0
|
||||
@@ -1,22 +0,0 @@
|
||||
# Copyright (c) 2021 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
# The public API.
|
||||
from .lupdate import lupdate
|
||||
|
||||
__all__ = ["lupdate"]
|
||||
@@ -1,90 +0,0 @@
|
||||
# Copyright (c) 2021 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
import fnmatch
|
||||
import os
|
||||
|
||||
# from .designer_source import DesignerSource
|
||||
from .python_source import PythonSource
|
||||
from .translation_file import TranslationFile
|
||||
from .user import UserException
|
||||
|
||||
|
||||
def lupdate(
|
||||
sources, translation_files, no_obsolete=False, no_summary=True, verbose=False, excludes=None
|
||||
):
|
||||
""" Update a sequence of translation (.ts) files from a sequence of Python
|
||||
source (.py) files, Designer source (.ui) files or directories containing
|
||||
source files.
|
||||
"""
|
||||
|
||||
if excludes is None:
|
||||
excludes = ()
|
||||
|
||||
# Read the .ts files.
|
||||
translations = [
|
||||
TranslationFile(ts, no_obsolete=no_obsolete, no_summary=no_summary, verbose=verbose)
|
||||
for ts in translation_files
|
||||
]
|
||||
|
||||
# Read the sources.
|
||||
source_files = []
|
||||
for source in sources:
|
||||
if os.path.isdir(source):
|
||||
for dirpath, _, filenames in os.walk(source):
|
||||
for fn in filenames:
|
||||
# Apply any exclusion patterns.
|
||||
for exclude in excludes:
|
||||
if fnmatch.fnmatch(fn, exclude):
|
||||
fn = None
|
||||
break
|
||||
|
||||
if fn is None:
|
||||
continue
|
||||
|
||||
filename = os.path.join(dirpath, fn)
|
||||
|
||||
if filename.endswith('.py'):
|
||||
source_files.append(PythonSource(filename=filename, verbose=verbose))
|
||||
|
||||
# UI file support is disabled
|
||||
# elif filename.endswith('.ui'):
|
||||
# source_files.append(
|
||||
# DesignerSource(filename=filename,
|
||||
# verbose=verbose))
|
||||
|
||||
elif verbose:
|
||||
print("Ignoring", filename)
|
||||
|
||||
elif source.endswith('.py'):
|
||||
source_files.append(PythonSource(filename=source, verbose=verbose))
|
||||
|
||||
# UI file support is disabled
|
||||
# elif source.endswith('.ui'):
|
||||
# source_files.append(
|
||||
# DesignerSource(filename=source, verbose=verbose))
|
||||
|
||||
else:
|
||||
raise UserException("{0} must be a directory or a .py or a .ui file".format(source))
|
||||
|
||||
# Update each translation for each source.
|
||||
for t in translations:
|
||||
for s in source_files:
|
||||
t.update(s)
|
||||
|
||||
t.write()
|
||||
@@ -1,362 +0,0 @@
|
||||
# Copyright (c) 2021 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
import ast
|
||||
import re
|
||||
import tokenize
|
||||
|
||||
from .source_file import SourceFile
|
||||
from .translations import Context, EmbeddedComments, Message
|
||||
from .user import User, UserException
|
||||
|
||||
|
||||
class PythonSource(SourceFile, User):
|
||||
""" Encapsulate a Python source file. """
|
||||
|
||||
# The regular expression to extract a PEP 263 encoding.
|
||||
_PEP_263 = re.compile(rb'^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Initialise the object. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Read the source file.
|
||||
self.progress("Reading {0}...".format(self.filename))
|
||||
with open(self.filename, 'rb') as f:
|
||||
source = f.read()
|
||||
|
||||
# Implement universal newlines.
|
||||
source = source.replace(b'\r\n', b'\n').replace(b'\r', b'\n')
|
||||
|
||||
# Try and extract a PEP 263 encoding.
|
||||
encoding = 'UTF-8'
|
||||
|
||||
for line_nr, line in enumerate(source.split(b'\n')):
|
||||
if line_nr > 1:
|
||||
break
|
||||
|
||||
match = re.match(self._PEP_263, line)
|
||||
if match:
|
||||
encoding = match.group(1).decode('ascii')
|
||||
break
|
||||
|
||||
# Decode the source according to the encoding.
|
||||
try:
|
||||
source = source.decode(encoding)
|
||||
except LookupError:
|
||||
raise UserException("Unsupported encoding '{0}'".format(encoding))
|
||||
|
||||
# Parse the source file.
|
||||
self.progress("Parsing {0}...".format(self.filename))
|
||||
|
||||
try:
|
||||
tree = ast.parse(source, filename=self.filename)
|
||||
except SyntaxError as e:
|
||||
raise UserException(
|
||||
"Invalid syntax at line {0} of {1}:\n{2}".format(
|
||||
e.lineno, e.filename, e.text.rstrip()
|
||||
)
|
||||
)
|
||||
|
||||
# Look for translation contexts and their contents.
|
||||
visitor = Visitor(self)
|
||||
visitor.visit(tree)
|
||||
|
||||
# Read the file again as a sequence of tokens so that we see the
|
||||
# comments.
|
||||
with open(self.filename, 'rb') as f:
|
||||
current = None
|
||||
|
||||
for token in tokenize.tokenize(f.readline):
|
||||
if token.type == tokenize.COMMENT:
|
||||
# See if it is an embedded comment.
|
||||
parts = token.string.split(' ', maxsplit=1)
|
||||
if len(parts) == 2:
|
||||
if parts[0] == '#:':
|
||||
if current is None:
|
||||
current = EmbeddedComments()
|
||||
|
||||
current.extra_comments.append(parts[1])
|
||||
elif parts[0] == '#=':
|
||||
if current is None:
|
||||
current = EmbeddedComments()
|
||||
|
||||
current.message_id = parts[1]
|
||||
elif parts[0] == '#~':
|
||||
parts = parts[1].split(' ', maxsplit=1)
|
||||
if len(parts) == 1:
|
||||
parts.append('')
|
||||
|
||||
if current is None:
|
||||
current = EmbeddedComments()
|
||||
|
||||
current.extras.append(parts)
|
||||
|
||||
elif token.type == tokenize.NL:
|
||||
continue
|
||||
|
||||
elif current is not None:
|
||||
# Associate the embedded comment with the line containing
|
||||
# this token.
|
||||
line_nr = token.start[0]
|
||||
|
||||
# See if there is a message on that line.
|
||||
for context in self.contexts:
|
||||
for message in context.messages:
|
||||
if message.line_nr == line_nr:
|
||||
break
|
||||
else:
|
||||
message = None
|
||||
|
||||
if message is not None:
|
||||
message.embedded_comments = current
|
||||
break
|
||||
|
||||
current = None
|
||||
|
||||
|
||||
class Visitor(ast.NodeVisitor):
|
||||
""" A visitor that extracts translation contexts. """
|
||||
|
||||
def __init__(self, source):
|
||||
""" Initialise the visitor. """
|
||||
|
||||
self._source = source
|
||||
self._context_stack = []
|
||||
|
||||
super().__init__()
|
||||
|
||||
def visit_Call(self, node):
|
||||
""" Visit a call. """
|
||||
|
||||
# Parse the arguments if a translation function is being called.
|
||||
call_args = None
|
||||
|
||||
if isinstance(node.func, ast.Attribute):
|
||||
name = node.func.attr
|
||||
|
||||
if name == 'tr':
|
||||
call_args = self._parse_tr(node)
|
||||
elif name == 'translate':
|
||||
call_args = self._parse_translate(node)
|
||||
|
||||
elif isinstance(node.func, ast.Name):
|
||||
name = node.func.id
|
||||
|
||||
if name == 'QT_TR_NOOP':
|
||||
call_args = self._parse_QT_TR_NOOP(node)
|
||||
elif name == 'QT_TRANSLATE_NOOP':
|
||||
call_args = self._parse_QT_TRANSLATE_NOOP(node)
|
||||
|
||||
# Update the context if the arguments are usable.
|
||||
if call_args is not None and call_args.source != '':
|
||||
call_args.context.messages.append(
|
||||
Message(
|
||||
self._source.filename, node.lineno,
|
||||
call_args.source, call_args.disambiguation,
|
||||
(call_args.numerus)
|
||||
)
|
||||
)
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
def visit_ClassDef(self, node):
|
||||
""" Visit a class. """
|
||||
|
||||
try:
|
||||
name = self._context_stack[-1].name + '.' + node.name
|
||||
except IndexError:
|
||||
name = node.name
|
||||
|
||||
self._context_stack.append(Context(name))
|
||||
|
||||
self.generic_visit(node)
|
||||
|
||||
context = self._context_stack.pop()
|
||||
|
||||
if context.messages:
|
||||
self._source.contexts.append(context)
|
||||
|
||||
def _get_current_context(self):
|
||||
""" Return the current Context object if there is one. """
|
||||
|
||||
return self._context_stack[-1] if self._context_stack else None
|
||||
|
||||
@classmethod
|
||||
def _get_first_str(cls, args):
|
||||
""" Get the first of a list of arguments as a str. """
|
||||
|
||||
# Check that there is at least one argument.
|
||||
if not args:
|
||||
return None
|
||||
|
||||
return cls._get_str(args[0])
|
||||
|
||||
def _get_or_create_context(self, name):
|
||||
""" Return the Context object for a name, creating it if necessary. """
|
||||
|
||||
for context in self._source.contexts:
|
||||
if context.name == name:
|
||||
return context
|
||||
|
||||
context = Context(name)
|
||||
self._source.contexts.append(context)
|
||||
|
||||
return context
|
||||
|
||||
@staticmethod
|
||||
def _get_str(node, allow_none=False):
|
||||
""" Return the str from a node or None if it wasn't an appropriate
|
||||
node.
|
||||
"""
|
||||
|
||||
if isinstance(node, ast.Str):
|
||||
return node.s
|
||||
|
||||
if isinstance(node, ast.Constant):
|
||||
if isinstance(node.value, str):
|
||||
return node.value
|
||||
|
||||
if allow_none and node.value is None:
|
||||
return ''
|
||||
|
||||
return None
|
||||
|
||||
def _parse_QT_TR_NOOP(self, node):
|
||||
""" Parse the arguments to QT_TR_NOOP(). """
|
||||
|
||||
# Ignore unless there is a current context.
|
||||
context = self._get_current_context()
|
||||
if context is None:
|
||||
return None
|
||||
|
||||
call_args = self._parse_noop_without_context(node.args, node.keywords)
|
||||
if call_args is None:
|
||||
return None
|
||||
|
||||
call_args.context = context
|
||||
|
||||
return call_args
|
||||
|
||||
def _parse_QT_TRANSLATE_NOOP(self, node):
|
||||
""" Parse the arguments to QT_TRANSLATE_NOOP(). """
|
||||
|
||||
# Get the context.
|
||||
name = self._get_first_str(node.args)
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
call_args = self._parse_noop_without_context(node.args[1:], node.keywords)
|
||||
if call_args is None:
|
||||
return None
|
||||
|
||||
call_args.context = self._get_or_create_context(name)
|
||||
|
||||
return call_args
|
||||
|
||||
def _parse_tr(self, node):
|
||||
""" Parse the arguments to tr(). """
|
||||
|
||||
# Ignore unless there is a current context.
|
||||
context = self._get_current_context()
|
||||
if context is None:
|
||||
return None
|
||||
|
||||
call_args = self._parse_without_context(node.args, node.keywords)
|
||||
if call_args is None:
|
||||
return None
|
||||
|
||||
call_args.context = context
|
||||
|
||||
return call_args
|
||||
|
||||
def _parse_translate(self, node):
|
||||
""" Parse the arguments to translate(). """
|
||||
|
||||
# Get the context.
|
||||
name = self._get_first_str(node.args)
|
||||
if name is None:
|
||||
return None
|
||||
|
||||
call_args = self._parse_without_context(node.args[1:], node.keywords)
|
||||
if call_args is None:
|
||||
return None
|
||||
|
||||
call_args.context = self._get_or_create_context(name)
|
||||
|
||||
return call_args
|
||||
|
||||
def _parse_without_context(self, args, keywords):
|
||||
""" Parse arguments for a message source and optional disambiguation
|
||||
and n.
|
||||
"""
|
||||
|
||||
# The source is required.
|
||||
source = self._get_first_str(args)
|
||||
if source is None:
|
||||
return None
|
||||
|
||||
if len(args) > 1:
|
||||
disambiguation = self._get_str(args[1], allow_none=True)
|
||||
else:
|
||||
for kw in keywords:
|
||||
if kw.arg == 'disambiguation':
|
||||
disambiguation = self._get_str(kw.value, allow_none=True)
|
||||
break
|
||||
else:
|
||||
disambiguation = ''
|
||||
|
||||
# Ignore if the disambiguation is specified but isn't a string.
|
||||
if disambiguation is None:
|
||||
return None
|
||||
|
||||
if len(args) > 2:
|
||||
numerus = True
|
||||
else:
|
||||
numerus = 'n' in keywords
|
||||
|
||||
if len(args) > 3:
|
||||
return None
|
||||
|
||||
return CallArguments(source, disambiguation, numerus)
|
||||
|
||||
def _parse_noop_without_context(self, args, keywords):
|
||||
""" Parse arguments for a message source. """
|
||||
|
||||
# There must be exactly one positional argument.
|
||||
if len(args) != 1 or len(keywords) != 0:
|
||||
return None
|
||||
|
||||
source = self._get_str(args[0])
|
||||
if source is None:
|
||||
return None
|
||||
|
||||
return CallArguments(source)
|
||||
|
||||
|
||||
class CallArguments:
|
||||
""" Encapsulate the possible arguments of a translation function. """
|
||||
|
||||
def __init__(self, source, disambiguation='', numerus=False):
|
||||
""" Initialise the object. """
|
||||
|
||||
self.context = None
|
||||
self.source = source
|
||||
self.disambiguation = disambiguation
|
||||
self.numerus = numerus
|
||||
@@ -1,29 +0,0 @@
|
||||
# Copyright (c) 2021 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
class SourceFile:
|
||||
""" The base class for any source file that provides translation contexts.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, **kwargs):
|
||||
""" Initialise the object. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.filename = filename
|
||||
self.contexts = []
|
||||
@@ -1,390 +0,0 @@
|
||||
# Copyright (c) 2021 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
import os
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from .user import User, UserException
|
||||
|
||||
|
||||
class TranslationFile(User):
|
||||
""" Encapsulate a translation file. """
|
||||
|
||||
def __init__(self, ts_file, no_obsolete, no_summary, **kwargs):
|
||||
""" Initialise the translation file. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
if os.path.isfile(ts_file):
|
||||
self.progress("Reading {0}...".format(ts_file))
|
||||
|
||||
try:
|
||||
self._root = ElementTree.parse(ts_file).getroot()
|
||||
except Exception as e:
|
||||
raise UserException(
|
||||
"{}: {}: {}".format(ts_file, "invalid translation file", str(e))
|
||||
)
|
||||
else:
|
||||
self._root = ElementTree.fromstring(_EMPTY_TS)
|
||||
|
||||
self._ts_file = ts_file
|
||||
self._no_obsolete = no_obsolete
|
||||
self._no_summary = no_summary
|
||||
self._updated_contexts = {}
|
||||
|
||||
# Create a dict of contexts keyed by the context name and having the
|
||||
# list of message elements as the value.
|
||||
self._contexts = {}
|
||||
|
||||
# Also create a dict of existing translations so that they can be
|
||||
# re-used.
|
||||
self._translations = {}
|
||||
|
||||
context_els = []
|
||||
for context_el in self._root:
|
||||
if context_el.tag != 'context':
|
||||
continue
|
||||
|
||||
context_els.append(context_el)
|
||||
|
||||
name = ''
|
||||
message_els = []
|
||||
|
||||
for el in context_el:
|
||||
if el.tag == 'name':
|
||||
name = el.text
|
||||
elif el.tag == 'message':
|
||||
message_els.append(el)
|
||||
|
||||
if name:
|
||||
self._contexts[name] = message_els
|
||||
|
||||
for message_el in message_els:
|
||||
source_el = message_el.find('source')
|
||||
if source_el is None or not source_el.text:
|
||||
continue
|
||||
|
||||
translation_el = message_el.find('translation')
|
||||
if translation_el is None or not translation_el.text:
|
||||
continue
|
||||
|
||||
self._translations[source_el.text] = translation_el.text
|
||||
|
||||
# Remove the context elements but keep everything else in the root
|
||||
# (probably set by Linguist).
|
||||
for context_el in context_els:
|
||||
self._root.remove(context_el)
|
||||
|
||||
# Clear the summary statistics.
|
||||
self._nr_new = 0
|
||||
self._nr_new_duplicates = 0
|
||||
self._nr_new_using_existing_translation = 0
|
||||
self._nr_existing = 0
|
||||
self._nr_kept_obsolete = 0
|
||||
self._nr_discarded_obsolete = 0
|
||||
self._nr_discarded_untranslated = 0
|
||||
|
||||
# Remember all new messages so we can make the summary less confusing
|
||||
# than it otherwise might be.
|
||||
self._new_message_els = []
|
||||
|
||||
def update(self, source):
|
||||
""" Update the translation file from a SourceFile object. """
|
||||
|
||||
self.progress("Updating {0} from {1}...".format(self._ts_file, source.filename))
|
||||
|
||||
for context in source.contexts:
|
||||
# Get the messages that we already know about for this context.
|
||||
try:
|
||||
message_els = self._contexts[context.name]
|
||||
except KeyError:
|
||||
message_els = []
|
||||
|
||||
# Get the messages that have already been updated.
|
||||
updated_message_els = self._get_updated_message_els(context.name)
|
||||
|
||||
for message in context.messages:
|
||||
message_el = self._find_message(message, message_els)
|
||||
|
||||
if message_el is not None:
|
||||
# Move the message to the updated list.
|
||||
message_els.remove(message_el)
|
||||
self._add_message_el(message_el, updated_message_els)
|
||||
else:
|
||||
# See if this is a new message. If not then we just have
|
||||
# another location for an existing message.
|
||||
message_el = self._find_message(message, updated_message_els)
|
||||
|
||||
if message_el is None:
|
||||
message_el = self._make_message_el(message)
|
||||
updated_message_els.append(message_el)
|
||||
|
||||
self.progress("Added new message '{0}'".format(self.pretty(message.source)))
|
||||
self._nr_new += 1
|
||||
else:
|
||||
self.progress("Updated message '{0}'".format(self.pretty(message.source)))
|
||||
|
||||
# Don't count another copy of a new message as an existing
|
||||
# one.
|
||||
if message_el in self._new_message_els:
|
||||
self._nr_new_duplicates += 1
|
||||
else:
|
||||
self._nr_existing += 1
|
||||
|
||||
message_el.insert(0, self._make_location_el(message))
|
||||
|
||||
def write(self):
|
||||
""" Write the translation file back to the filesystem. """
|
||||
|
||||
# If we are keeping obsolete messages then add them to the updated
|
||||
# message elements list.
|
||||
for name, message_els in self._contexts.items():
|
||||
updated_message_els = None
|
||||
|
||||
for message_el in message_els:
|
||||
source = self.pretty(message_el.find('source').text)
|
||||
|
||||
translation_el = message_el.find('translation')
|
||||
if translation_el is not None and translation_el.text:
|
||||
if self._no_obsolete:
|
||||
self.progress("Discarded obsolete message '{0}'".format(source))
|
||||
self._nr_discarded_obsolete += 1
|
||||
else:
|
||||
translation_el.set('type', 'vanished')
|
||||
|
||||
if updated_message_els is None:
|
||||
updated_message_els = self._get_updated_message_els(name)
|
||||
|
||||
self._add_message_el(message_el, updated_message_els)
|
||||
|
||||
self.progress("Kept obsolete message '{0}'".format(source))
|
||||
self._nr_kept_obsolete += 1
|
||||
else:
|
||||
self.progress("Discarded untranslated message '{0}'".format(source))
|
||||
self._nr_discarded_untranslated += 1
|
||||
|
||||
# Created the sorted context elements.
|
||||
for name in sorted(self._updated_contexts.keys()):
|
||||
context_el = ElementTree.Element('context')
|
||||
|
||||
name_el = ElementTree.Element('name')
|
||||
name_el.text = name
|
||||
context_el.append(name_el)
|
||||
|
||||
context_el.extend(self._updated_contexts[name])
|
||||
|
||||
self._root.append(context_el)
|
||||
|
||||
self.progress("Writing {0}...".format(self._ts_file))
|
||||
with open(self._ts_file, 'w', encoding='utf-8', newline='\n') as f:
|
||||
f.write('<?xml version="1.0" encoding="utf-8"?>\n')
|
||||
f.write('<!DOCTYPE TS>\n')
|
||||
|
||||
# Python v3.9 and later.
|
||||
if hasattr(ElementTree, 'indent'):
|
||||
ElementTree.indent(self._root)
|
||||
|
||||
ElementTree.ElementTree(self._root).write(f, encoding='unicode')
|
||||
f.write('\n')
|
||||
|
||||
if not self._no_summary:
|
||||
self._summary()
|
||||
|
||||
@staticmethod
|
||||
def _add_message_el(message_el, updated_message_els):
|
||||
""" Add a message element to a list of updated message elements. """
|
||||
|
||||
# Remove all the location elements.
|
||||
for location_el in message_el.findall('location'):
|
||||
message_el.remove(location_el)
|
||||
|
||||
# Add the message to the updated list.
|
||||
updated_message_els.append(message_el)
|
||||
|
||||
@classmethod
|
||||
def _find_message(cls, message, message_els):
|
||||
""" Return the message element for a message from a list. """
|
||||
|
||||
for message_el in message_els:
|
||||
source = ''
|
||||
comment = ''
|
||||
extra_comment = ''
|
||||
extras = []
|
||||
|
||||
# Extract the data from the element.
|
||||
for el in message_el:
|
||||
if el.tag == 'source':
|
||||
source = el.text
|
||||
elif el.tag == 'comment':
|
||||
comment = el.text
|
||||
elif el.tag == 'extracomment':
|
||||
extra_comment = el.text
|
||||
elif el.tag.startswith('extra-'):
|
||||
extras.append([el.tag[6:], el.text])
|
||||
|
||||
# Compare with the message.
|
||||
if source != message.source:
|
||||
continue
|
||||
|
||||
if comment != message.comment:
|
||||
continue
|
||||
|
||||
if extra_comment != cls._get_message_extra_comments(message):
|
||||
continue
|
||||
|
||||
if extras != message.embedded_comments.extras:
|
||||
continue
|
||||
|
||||
return message_el
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_message_extra_comments(message):
|
||||
""" Return a message's extra comments as they appear in a .ts file. """
|
||||
|
||||
return ' '.join(message.embedded_comments.extra_comments)
|
||||
|
||||
def _get_updated_message_els(self, name):
|
||||
""" Return the list of updated message elements for a context. """
|
||||
|
||||
try:
|
||||
updated_message_els = self._updated_contexts[name]
|
||||
except KeyError:
|
||||
updated_message_els = []
|
||||
self._updated_contexts[name] = updated_message_els
|
||||
|
||||
return updated_message_els
|
||||
|
||||
def _make_location_el(self, message):
|
||||
""" Return a 'location' element. """
|
||||
|
||||
return ElementTree.Element(
|
||||
'location',
|
||||
filename=os.path.relpath(
|
||||
message.filename, start=os.path.dirname(os.path.abspath(self._ts_file))
|
||||
),
|
||||
line=str(message.line_nr)
|
||||
)
|
||||
|
||||
def _make_message_el(self, message):
|
||||
""" Return a 'message' element. """
|
||||
|
||||
attrs = {}
|
||||
|
||||
if message.embedded_comments.message_id:
|
||||
attrs['id'] = message.embedded_comments.message_id
|
||||
|
||||
if message.numerus:
|
||||
attrs['numerus'] = 'yes'
|
||||
|
||||
message_el = ElementTree.Element('message', attrs)
|
||||
|
||||
source_el = ElementTree.Element('source')
|
||||
source_el.text = message.source
|
||||
message_el.append(source_el)
|
||||
|
||||
if message.comment:
|
||||
comment_el = ElementTree.Element('comment')
|
||||
comment_el.text = message.comment
|
||||
message_el.append(comment_el)
|
||||
|
||||
if message.embedded_comments.extra_comments:
|
||||
extracomment_el = ElementTree.Element('extracomment')
|
||||
extracomment_el.text = self._get_message_extra_comments(message)
|
||||
message_el.append(extracomment_el)
|
||||
|
||||
translation_el = ElementTree.Element('translation', type='unfinished')
|
||||
|
||||
# Try and find another message with the same source and use its
|
||||
# translation if it has one.
|
||||
translation = self._translations.get(message.source)
|
||||
if translation:
|
||||
translation_el.text = translation
|
||||
|
||||
self.progress("Reused existing translation for '{0}'".format(
|
||||
self.pretty(message.source))
|
||||
)
|
||||
self._nr_new_using_existing_translation += 1
|
||||
|
||||
if message.numerus:
|
||||
translation_el.append(ElementTree.Element('numerusform'))
|
||||
|
||||
message_el.append(translation_el)
|
||||
|
||||
for field, value in message.embedded_comments.extras:
|
||||
el = ElementTree.Element('extra-' + field)
|
||||
el.text = value
|
||||
message_el.append(el)
|
||||
|
||||
self._new_message_els.append(message_el)
|
||||
|
||||
return message_el
|
||||
|
||||
def _summary(self):
|
||||
""" Display the summary of changes to the user. """
|
||||
|
||||
summary_lines = []
|
||||
|
||||
# Display a line of the summary and the heading if not already done.
|
||||
def summary(line):
|
||||
nonlocal summary_lines
|
||||
|
||||
if not summary_lines:
|
||||
summary_lines.append("Summary of changes to {ts}:".format(ts=self._ts_file))
|
||||
|
||||
summary_lines.append(" " + line)
|
||||
|
||||
if self._nr_new:
|
||||
if self._nr_new_duplicates:
|
||||
summary("{0} new messages were added (and {1} duplicates)".format(
|
||||
self._nr_new, self._nr_new_duplicates))
|
||||
else:
|
||||
summary("{0} new messages were added".format(self._nr_new))
|
||||
|
||||
if self._nr_new_using_existing_translation:
|
||||
summary("{0} messages reused existing translations".format(
|
||||
self._nr_new_using_existing_translation))
|
||||
|
||||
if self._nr_existing:
|
||||
summary("{0} existing messages were found".format(
|
||||
self._nr_existing))
|
||||
|
||||
if self._nr_kept_obsolete:
|
||||
summary("{0} obsolete messages were kept".format(
|
||||
self._nr_kept_obsolete))
|
||||
|
||||
if self._nr_discarded_obsolete:
|
||||
summary("{0} obsolete messages were discarded".format(
|
||||
self._nr_discarded_obsolete))
|
||||
|
||||
if self._nr_discarded_untranslated:
|
||||
summary("{0} untranslated messages were discarded".format(
|
||||
self._nr_discarded_untranslated))
|
||||
|
||||
if not summary_lines:
|
||||
summary_lines.append("{ts} was unchanged".format(ts=self._ts_file))
|
||||
|
||||
print(os.linesep.join(summary_lines))
|
||||
|
||||
|
||||
# The XML of an empty .ts file. This is what a current lupdate will create
|
||||
# with an empty C++ source file.
|
||||
_EMPTY_TS = '''<TS version="2.1">
|
||||
</TS>
|
||||
'''
|
||||
@@ -1,51 +0,0 @@
|
||||
# Copyright (c) 2021 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
class Context:
|
||||
""" Encapsulate a message context. """
|
||||
|
||||
def __init__(self, name):
|
||||
""" Initialise the context. """
|
||||
|
||||
self.name = name
|
||||
self.messages = []
|
||||
|
||||
|
||||
class EmbeddedComments:
|
||||
""" Encapsulate information for a translator embedded in comments. """
|
||||
|
||||
def __init__(self):
|
||||
""" Initialise the object. """
|
||||
|
||||
self.message_id = ''
|
||||
self.extra_comments = []
|
||||
self.extras = []
|
||||
|
||||
|
||||
class Message:
|
||||
""" Encapsulate a message. """
|
||||
|
||||
def __init__(self, filename, line_nr, source, comment, numerus):
|
||||
""" Initialise the message. """
|
||||
|
||||
self.filename = filename
|
||||
self.line_nr = line_nr
|
||||
self.source = source
|
||||
self.comment = comment
|
||||
self.numerus = numerus
|
||||
self.embedded_comments = EmbeddedComments()
|
||||
@@ -1,47 +0,0 @@
|
||||
# Copyright (c) 2021 Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
#
|
||||
# This file is part of PyQt6.
|
||||
#
|
||||
# This file may be used under the terms of the GNU General Public License
|
||||
# version 3.0 as published by the Free Software Foundation and appearing in
|
||||
# the file LICENSE included in the packaging of this file. Please review the
|
||||
# following information to ensure the GNU General Public License version 3.0
|
||||
# requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
#
|
||||
# If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
# then you may purchase a commercial license. For more information contact
|
||||
# info@riverbankcomputing.com.
|
||||
#
|
||||
# This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
# WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
|
||||
class UserException(Exception):
|
||||
""" Encapsulate an exception ultimate caused by the user. """
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class User:
|
||||
""" A mixin that provides methods for communicating with the user. """
|
||||
|
||||
def __init__(self, verbose, **kwargs):
|
||||
""" Initialise the object. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._verbose = verbose
|
||||
|
||||
@staticmethod
|
||||
def pretty(text):
|
||||
""" Returns a pretty-fied version of some text suitable for displaying
|
||||
to the user.
|
||||
"""
|
||||
|
||||
return text.replace('\n', '\\n')
|
||||
|
||||
def progress(self, message):
|
||||
""" Display a progress message. """
|
||||
|
||||
if self._verbose:
|
||||
print(message)
|
||||
+1
-2
@@ -1,4 +1,3 @@
|
||||
# Minimal PEP 518 pyproject file
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
@@ -7,7 +7,7 @@ description = A markdown-like text editor for planning and writing novels
|
||||
url = https://novelwriter.io
|
||||
long_description = file: setup/description_pypi.md
|
||||
long_description_content_type = text/markdown
|
||||
license_file = LICENSE.md
|
||||
license_files = LICENSE.md
|
||||
license = GNU General Public License v3
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
@@ -29,9 +29,10 @@ project_urls =
|
||||
Source Code = https://github.com/vkbo/novelWriter
|
||||
|
||||
[options]
|
||||
zip_safe = False
|
||||
python_requires = >=3.7
|
||||
include_package_data = True
|
||||
packages = find:
|
||||
packages = find_namespace:
|
||||
install_requires =
|
||||
pyqt5>=5.10
|
||||
lxml>=4.2.0
|
||||
|
||||
@@ -73,12 +73,10 @@ def extractVersion(beQuiet=False):
|
||||
return numVers, hexVers, relDate
|
||||
|
||||
|
||||
def compactVersion(numVers):
|
||||
"""Make the version number more compact."""
|
||||
numVers = numVers.replace("-alpha", "a")
|
||||
numVers = numVers.replace("-beta", "b")
|
||||
numVers = numVers.replace("-rc", "rc")
|
||||
return numVers
|
||||
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):
|
||||
@@ -95,20 +93,21 @@ def sysCall(callArgs, cwd=None):
|
||||
def readFile(fileName):
|
||||
"""Read an entire file and return as a string.
|
||||
"""
|
||||
with open(fileName, mode="r") as inFile:
|
||||
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+") as outFile:
|
||||
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."""
|
||||
directory. The file can optionally be given a new name.
|
||||
"""
|
||||
uplDir = "dist_upload"
|
||||
if not os.path.isdir(uplDir):
|
||||
os.mkdir(uplDir)
|
||||
@@ -127,7 +126,7 @@ def makeCheckSum(sumFile, cwd=None):
|
||||
else:
|
||||
shaFile = os.path.join(cwd, sumFile+".sha256")
|
||||
with open(shaFile, mode="w") as fOut:
|
||||
subprocess.call(["sha256sum", sumFile], stdout=fOut, cwd=cwd)
|
||||
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")
|
||||
@@ -331,6 +330,12 @@ def buildQtI18nTS(sysArgs):
|
||||
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("")
|
||||
@@ -386,10 +391,7 @@ def buildQtI18nTS(sysArgs):
|
||||
print("Updating Language Files:")
|
||||
print("")
|
||||
|
||||
# Using the pylupdate tool from PyQt6 instead as it supports TS file
|
||||
# format 2.1. This can perhaps be changed back to the installed tool
|
||||
# at a later time.
|
||||
from i18n.pylupdate6 import lupdate
|
||||
# Using the pylupdate tool from PyQt6 as it supports TS file format 2.1.
|
||||
lupdate(srcList, tsList, no_obsolete=True, no_summary=False)
|
||||
|
||||
print("")
|
||||
@@ -402,7 +404,7 @@ def buildQtI18nTS(sysArgs):
|
||||
##
|
||||
|
||||
def genMacOSPlist():
|
||||
"""Set necessary values for .plist file.
|
||||
"""Set necessary values for .plist file for MacOS build.
|
||||
"""
|
||||
numVers, _, _ = extractVersion()
|
||||
pkgVers = compactVersion(numVers)
|
||||
@@ -936,11 +938,10 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
|
||||
##
|
||||
|
||||
def makeAppImage(sysArgs):
|
||||
"""Build an Appimage.
|
||||
"""Build an AppImage.
|
||||
"""
|
||||
import glob
|
||||
import argparse
|
||||
import platform
|
||||
|
||||
try:
|
||||
import python_appimage # noqa F401
|
||||
@@ -964,15 +965,15 @@ def makeAppImage(sysArgs):
|
||||
parser.add_argument(
|
||||
"--linux-tag",
|
||||
nargs="?",
|
||||
default=f"manylinux2010_{platform.machine()}",
|
||||
default="manylinux_2_28_x86_64",
|
||||
help=(
|
||||
"linux compatibility tag (e.g. manylinux1_x86_64) \n"
|
||||
"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.10", help="python version (e.g. 3.10)"
|
||||
"--python-version", nargs="?", default="3.11", help="Python version (e.g. 3.11)"
|
||||
)
|
||||
|
||||
args, unparsedArgs = parser.parse_known_args(sysArgs)
|
||||
@@ -1016,9 +1017,8 @@ def makeAppImage(sysArgs):
|
||||
|
||||
os.mkdir(imageDir)
|
||||
|
||||
# Remove old Appimages
|
||||
# Remove old AppImages
|
||||
outFiles = glob.glob(f"{bldDir}/*.AppImage")
|
||||
|
||||
if outFiles:
|
||||
print("Removing old AppImages")
|
||||
print("")
|
||||
@@ -1108,17 +1108,17 @@ def makeAppImage(sysArgs):
|
||||
print("Wrote: requirements.txt")
|
||||
|
||||
shutil.copyfile("setup/data/novelwriter.desktop", f"{imageDir}/novelwriter.desktop")
|
||||
print("Copied: setup/data/novelwriter.desktop")
|
||||
print("Copied: novelwriter.desktop")
|
||||
|
||||
shutil.copyfile("setup/icons/novelwriter.svg", f"{imageDir}/novelwriter.svg")
|
||||
print("Copied: setup/icons/novelwriter.svg")
|
||||
print("Copied: novelwriter.svg")
|
||||
|
||||
shutil.copyfile(
|
||||
"setup/data/hicolor/256x256/apps/novelwriter.png", f"{imageDir}/novelwriter.png"
|
||||
)
|
||||
print("Copied: setup/data/hicolor/256x256/apps/novelwriter.png")
|
||||
print("Copied: novelwriter.png")
|
||||
|
||||
# Build Appimage
|
||||
# Build AppImage
|
||||
# ==============
|
||||
|
||||
try:
|
||||
@@ -1131,13 +1131,17 @@ def makeAppImage(sysArgs):
|
||||
print("")
|
||||
print(str(exc))
|
||||
print("")
|
||||
print("Dependencies:")
|
||||
print(" * pip install python-appimage")
|
||||
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}-py{pythonVer}-{linuxTag}.AppImage"
|
||||
outFile = f"{bldDir}/novelWriter-{pkgVers}{linuxLabel}.AppImage"
|
||||
os.rename(bldFile, outFile)
|
||||
shaFile = makeCheckSum(os.path.basename(outFile), cwd=bldDir)
|
||||
|
||||
@@ -1368,11 +1372,8 @@ def makeWindowsEmbedded(sysArgs):
|
||||
print(str(exc))
|
||||
sys.exit(1)
|
||||
|
||||
issName = os.path.join("dist", f"novelwriter-{packVersion}-win10-amd64-setup.exe")
|
||||
newName = os.path.join("dist", f"novelwriter-{packVersion}-py{pyVers}-win10-amd64-setup.exe")
|
||||
os.replace(issName, newName)
|
||||
|
||||
print(f"Installer: {newName}")
|
||||
print("")
|
||||
print("Done")
|
||||
print("")
|
||||
|
||||
return
|
||||
@@ -1904,13 +1905,11 @@ if __name__ == "__main__":
|
||||
" 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",
|
||||
" manylinux1_x86_64 / i386, and --python-version to 3.10.",
|
||||
" manylinux_2_28_x86_64, and --python-version to 3.11.",
|
||||
"",
|
||||
"System Install:",
|
||||
"",
|
||||
" install Installs novelWriter to the system's Python install",
|
||||
" location. Run as root or with sudo for system-wide install,",
|
||||
" or as user for single user 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.",
|
||||
@@ -2063,6 +2062,14 @@ if __name__ == "__main__":
|
||||
|
||||
# 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
|
||||
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Please provide the path to the novelWriter.AppImage file as an argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -e "$1" ]; then
|
||||
echo "File not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMGPATH="$(readlink -m "$1")"
|
||||
echo "AppImage: $IMGPATH"
|
||||
|
||||
ICONPATH="$(dirname "$IMGPATH")/novelWriter.png"
|
||||
|
||||
if [ ! -e "$ICONPATH" ]; then
|
||||
echo "Downloading icon"
|
||||
wget https://raw.githubusercontent.com/vkbo/novelWriter/main/setup/data/hicolor/256x256/apps/novelwriter.png -O "$ICONPATH"
|
||||
fi
|
||||
|
||||
cat > $HOME/.local/share/applications/novelWriter.desktop <<EOF
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=novelWriter
|
||||
Comment=A markdown-like text editor for planning and writing novels
|
||||
Exec=${IMGPATH} %f
|
||||
Icon=${ICONPATH}
|
||||
Categories=Qt;Office;WordProcessor;
|
||||
Terminal=false
|
||||
EOF
|
||||
|
||||
update-desktop-database ~/.local/share/applications
|
||||
|
||||
echo "Desktop launcher generated"
|
||||
+1
-17
@@ -4,7 +4,7 @@ Upstream-Contact: Veronica Berglyd Olsen <code@vkbo.net>
|
||||
Source: https://github.com/vkbo/novelWriter
|
||||
|
||||
Files: *
|
||||
Copyright: 2018-2021, Veronica Berglyd Olsen
|
||||
Copyright: 2018-2023, Veronica Berglyd Olsen
|
||||
License: GPL-3.0-or-later
|
||||
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
|
||||
@@ -19,22 +19,6 @@ License: GPL-3.0-or-later
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Files: i18n/pylupdate6/*.py
|
||||
Copyright: 2021, Riverbank Computing Limited <info@riverbankcomputing.com>
|
||||
License: GPL-3.0
|
||||
This file may be used under the terms of the GNU General Public License
|
||||
version 3.0 as published by the Free Software Foundation and appearing in
|
||||
the file LICENSE included in the packaging of this file. Please review the
|
||||
following information to ensure the GNU General Public License version 3.0
|
||||
requirements will be met: http://www.gnu.org/copyleft/gpl.html.
|
||||
.
|
||||
If you do not wish to use this file under the terms of the GPL version 3.0
|
||||
then you may purchase a commercial license. For more information contact
|
||||
info@riverbankcomputing.com.
|
||||
.
|
||||
This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
|
||||
WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
|
||||
|
||||
Files: novelwriter/assets/icons/typicons_*.svg
|
||||
Copyright: 2019, Stephen Hutchings
|
||||
License: CC-BY-SA-4.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
novelWriter License
|
||||
|
||||
Copyright (C) 2018-2022 Veronica Berglyd Olsen
|
||||
Copyright (C) 2018-2023 Veronica Berglyd Olsen
|
||||
License: GPL v3+ <https://www.gnu.org/licenses/gpl-3.0.html>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
|
||||
+19
-29
@@ -12,8 +12,6 @@ SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
SRC_DIR="$SCRIPT_DIR/../.."
|
||||
RLS_DIR="$SRC_DIR/dist_macos"
|
||||
|
||||
echo "Script Dir: $SCRIPT_DIR"
|
||||
|
||||
cleanup () {
|
||||
if [ -d "$BUILD_DIR" ]; then
|
||||
rm -rf "$BUILD_DIR"
|
||||
@@ -21,13 +19,14 @@ cleanup () {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Building in: $BUILD_DIR"
|
||||
|
||||
OLD_CWD="$(pwd)"
|
||||
VERSION="$(awk '/^__version__/{print substr($NF,2,length($NF)-2)}' $SRC_DIR/novelwriter/__init__.py)"
|
||||
echo "Script Dir: $SCRIPT_DIR"
|
||||
echo "Build Dir: $BUILD_DIR"
|
||||
|
||||
pushd "$SRC_DIR" || exit 1
|
||||
|
||||
VERSION="$(python3 setup.py version)"
|
||||
echo "novelWriter Version: $VERSION"
|
||||
|
||||
# --- Prepare Files ----------------------------------------------------------------------------- #
|
||||
|
||||
echo "Generating Info.plist"
|
||||
@@ -68,23 +67,21 @@ pushd "$BUILD_DIR"/ || exit 1
|
||||
|
||||
# --- Create Miniconda Env ---------------------------------------------------------------------- #
|
||||
|
||||
# install Miniconda, a self-contained Python distribution
|
||||
echo "Downloading Miniconda ..."
|
||||
curl -LO https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh
|
||||
bash Miniconda3-latest-MacOSX-x86_64.sh -b -p ~/miniconda -f
|
||||
rm Miniconda3-latest-MacOSX-x86_64.sh
|
||||
export PATH="$HOME/miniconda/bin:$PATH"
|
||||
|
||||
echo "Creating conda env ..."
|
||||
# create conda env
|
||||
echo "Creating Conda env ..."
|
||||
conda create -n novelWriter -c conda-forge python=3.10 --yes
|
||||
source activate novelWriter
|
||||
|
||||
echo "installing dictionaries ..."
|
||||
echo "Installing dictionaries ..."
|
||||
conda install -c conda-forge enchant hunspell-en --yes
|
||||
|
||||
# Install dependencies
|
||||
echo "installing python dependencies ..."
|
||||
echo "Installing Python dependencies ..."
|
||||
pip install -r "$SRC_DIR/requirements.txt"
|
||||
|
||||
# Leave conda env
|
||||
@@ -92,8 +89,9 @@ conda deactivate
|
||||
|
||||
# --- Build App --------------------------------------------------------------------------------- #
|
||||
|
||||
echo "Building app bundle ..."
|
||||
# create .app Framework
|
||||
echo "Building App bundle ..."
|
||||
|
||||
# Create .app Framework
|
||||
mkdir -p novelWriter.app/Contents/
|
||||
mkdir novelWriter.app/Contents/MacOS novelWriter.app/Contents/Resources novelWriter.app/Contents/Resources/novelWriter
|
||||
cp $SRC_DIR/setup/macos/Info.plist novelWriter.app/Contents/Info.plist
|
||||
@@ -130,13 +128,11 @@ chmod a+x novelWriter.app/Contents/MacOS/novelWriter
|
||||
# echo "Signing bundle ..."
|
||||
# sudo codesign --sign - --deep --force --entitlements "$SCRIPT_DIR/../macos/App.entitlements" --options runtime "novelWriter.app/Contents/MacOS/novelWriter"
|
||||
|
||||
# Remove bloat
|
||||
pushd novelWriter.app/Contents/Resources || exit 1
|
||||
|
||||
# --- Cleanup ----------------------------------------------------------------------------------- #
|
||||
|
||||
echo "Cleaning unused files from bundle ..."
|
||||
# cleanup commands HERE
|
||||
|
||||
pushd novelWriter.app/Contents/Resources || exit 1
|
||||
find . -type d -iname '__pycache__' -print0 | xargs -0 rm -r
|
||||
|
||||
rm -rf pkgs
|
||||
@@ -146,7 +142,7 @@ rm -rf share/{gtk-,}doc
|
||||
# Remove the files from the 3.1 symlink
|
||||
rm -rf lib/python3.1
|
||||
|
||||
# remove web engine
|
||||
# Remove web engine
|
||||
rm lib/python3.*/site-packages/PyQt5/QtWebEngine* || true
|
||||
rm -r lib/python3.*/site-packages/PyQt5/Qt/translations/qtwebengine* || true
|
||||
rm lib/python3.*/site-packages/PyQt5/Qt/resources/qtwebengine* || true
|
||||
@@ -158,25 +154,19 @@ rm lib/python3.*/site-packages/PyQt5/Qt/lib/libQt5WebEngine* || true
|
||||
popd || exit 1
|
||||
popd || exit 1
|
||||
|
||||
# --- Create App Bundle-------------------------------------------------------------------------- #
|
||||
|
||||
echo "Packageing App ..."
|
||||
mkdir -p $RLS_DIR
|
||||
|
||||
pushd $BUILD_DIR || exit 1
|
||||
zip -qr novelWriter.app.zip novelWriter.app
|
||||
popd || exit 1
|
||||
|
||||
mv -v $BUILD_DIR/novelWriter.app.zip $RLS_DIR/novelWriter-"${VERSION}"-macos.app.zip
|
||||
|
||||
# --- Create DMG -------------------------------------------------------------------------------- #
|
||||
|
||||
# Generate .dmg
|
||||
echo "Packageing DMG ..."
|
||||
brew install create-dmg
|
||||
|
||||
# "--skip-jenkins" is a temporary workaround for https://github.com/create-dmg/create-dmg/issues/72
|
||||
create-dmg --volname "novelWriter $VERSION" --volicon $SRC_DIR/setup/macos/novelwriter.icns \
|
||||
--window-pos 200 120 --window-size 800 400 --icon-size 100 \
|
||||
--icon novelWriter.app 200 190 --hide-extension novelWriter.app \
|
||||
--app-drop-link 600 185 $RLS_DIR/novelWriter-"${VERSION}"-macos.dmg "$BUILD_DIR"/
|
||||
--app-drop-link 600 185 $RLS_DIR/novelWriter-"${VERSION}".dmg "$BUILD_DIR"/
|
||||
|
||||
pushd $RLS_DIR || exit 1
|
||||
shasum -a 256 novelWriter-"${VERSION}".dmg | tee novelWriter-"${VERSION}".dmg.sha256
|
||||
popd || exit 1
|
||||
|
||||
+5
-1
@@ -11,9 +11,13 @@ echo " Building Packages"
|
||||
echo "================================================================================"
|
||||
echo ""
|
||||
python3 setup.py qtlrelease manual sample
|
||||
python3 setup.py sdist bdist_wheel
|
||||
python3 -m build
|
||||
mkdir -pv dist_upload
|
||||
cp -v dist/novelWriter-*.whl dist_upload/
|
||||
cd dist_upload
|
||||
FILE=$(ls -t | head -1)
|
||||
shasum -a 256 $FILE | tee $FILE.sha256
|
||||
cd ..
|
||||
|
||||
echo ""
|
||||
echo " Checking Packages"
|
||||
|
||||
@@ -24,7 +24,7 @@ DisableProgramGroupPage=yes
|
||||
UsedUserAreasWarning=no
|
||||
PrivilegesRequiredOverridesAllowed=dialog
|
||||
OutputDir={#nwAppDir}
|
||||
OutputBaseFilename=novelwriter-{#nwAppVersion}-win10-amd64-setup
|
||||
OutputBaseFilename=novelwriter-{#nwAppVersion}-amd64-setup
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
WizardStyle=modern
|
||||
|
||||
Reference in New Issue
Block a user