Merge branch 'dev' into feature/build_gui
This commit is contained in:
@@ -42,7 +42,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.10"]
|
python-version: ["3.11"]
|
||||||
linux-tag: ["manylinux_2_24_x86_64", "manylinux_2_28_x86_64"]
|
linux-tag: ["manylinux_2_24_x86_64", "manylinux_2_28_x86_64"]
|
||||||
steps:
|
steps:
|
||||||
- name: Python Setup
|
- name: Python Setup
|
||||||
@@ -57,13 +57,6 @@ jobs:
|
|||||||
- name: Checkout Source
|
- name: Checkout Source
|
||||||
uses: actions/checkout@v3
|
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
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -76,27 +69,20 @@ jobs:
|
|||||||
- name: Upload Artifacts
|
- name: Upload Artifacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: novelWriter-${{ env.VERSION }}-py${{ matrix.python-version }}-${{ matrix.linux-tag }}.AppImage
|
name: novelWriter-Packages
|
||||||
path: dist_appimage/novelWriter-${{ env.VERSION }}-py${{ matrix.python-version }}-${{ matrix.linux-tag }}.AppImage
|
path: |
|
||||||
|
dist_appimage/novelWriter*.AppImage
|
||||||
|
dist_appimage/novelWriter*.AppImage.sha256
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
buildMac:
|
buildMac:
|
||||||
needs: buildAssets
|
needs: buildAssets
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
runs-on: macos-12
|
runs-on: macos-12
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
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
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
@@ -106,17 +92,12 @@ jobs:
|
|||||||
- name: Build App Bundle
|
- name: Build App Bundle
|
||||||
run: ./setup/macos/build.sh
|
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
|
- name: Upload DMG
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: novelWriter-${{ env.VERSION }}-macos.dmg
|
name: novelWriter-Packages
|
||||||
path: dist_macos/novelWriter-${{ env.VERSION }}-macos.dmg
|
path: |
|
||||||
|
dist_macos/novelWriter*.dmg
|
||||||
|
dist_macos/novelWriter*.dmg.sha256
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
retention-days: 1
|
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
|
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``.
|
``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:
|
.. _a_source_depend:
|
||||||
|
|
||||||
@@ -58,37 +74,35 @@ source, dependencies can still be installed from PyPi with:
|
|||||||
|
|
||||||
.. _a_source_install:
|
.. _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
|
If you want to install novelWriter directly from the source available on GitHub_, you must first
|
||||||
the setup manually. It is equivalent to what the ``pip install`` command does, and it installs
|
build the package using the Python Packaging Authority's build tool. It can be installed with:
|
||||||
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:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. 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
|
.. 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
|
.. 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
|
.. code-block:: bash
|
||||||
running ``python setup.py help``.
|
|
||||||
|
pip install --user dist/novelWriter-2.0.7-py3-none-any.whl
|
||||||
|
|
||||||
|
|
||||||
.. _a_source_i18n:
|
.. _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]
|
[build-system]
|
||||||
requires = ["setuptools", "wheel"]
|
requires = ["setuptools"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ description = A markdown-like text editor for planning and writing novels
|
|||||||
url = https://novelwriter.io
|
url = https://novelwriter.io
|
||||||
long_description = file: setup/description_pypi.md
|
long_description = file: setup/description_pypi.md
|
||||||
long_description_content_type = text/markdown
|
long_description_content_type = text/markdown
|
||||||
license_file = LICENSE.md
|
license_files = LICENSE.md
|
||||||
license = GNU General Public License v3
|
license = GNU General Public License v3
|
||||||
classifiers =
|
classifiers =
|
||||||
Programming Language :: Python :: 3 :: Only
|
Programming Language :: Python :: 3 :: Only
|
||||||
@@ -29,9 +29,10 @@ project_urls =
|
|||||||
Source Code = https://github.com/vkbo/novelWriter
|
Source Code = https://github.com/vkbo/novelWriter
|
||||||
|
|
||||||
[options]
|
[options]
|
||||||
|
zip_safe = False
|
||||||
python_requires = >=3.7
|
python_requires = >=3.7
|
||||||
include_package_data = True
|
include_package_data = True
|
||||||
packages = find:
|
packages = find_namespace:
|
||||||
install_requires =
|
install_requires =
|
||||||
pyqt5>=5.10
|
pyqt5>=5.10
|
||||||
lxml>=4.2.0
|
lxml>=4.2.0
|
||||||
|
|||||||
@@ -73,12 +73,10 @@ def extractVersion(beQuiet=False):
|
|||||||
return numVers, hexVers, relDate
|
return numVers, hexVers, relDate
|
||||||
|
|
||||||
|
|
||||||
def compactVersion(numVers):
|
def compactVersion(version):
|
||||||
"""Make the version number more compact."""
|
"""Make the version number more compact.
|
||||||
numVers = numVers.replace("-alpha", "a")
|
"""
|
||||||
numVers = numVers.replace("-beta", "b")
|
return version.replace("-alpha", "a").replace("-beta", "b").replace("-rc", "rc")
|
||||||
numVers = numVers.replace("-rc", "rc")
|
|
||||||
return numVers
|
|
||||||
|
|
||||||
|
|
||||||
def sysCall(callArgs, cwd=None):
|
def sysCall(callArgs, cwd=None):
|
||||||
@@ -95,20 +93,21 @@ def sysCall(callArgs, cwd=None):
|
|||||||
def readFile(fileName):
|
def readFile(fileName):
|
||||||
"""Read an entire file and return as a string.
|
"""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()
|
return inFile.read()
|
||||||
|
|
||||||
|
|
||||||
def writeFile(fileName, writeText):
|
def writeFile(fileName, writeText):
|
||||||
"""Write string to file.
|
"""Write string to file.
|
||||||
"""
|
"""
|
||||||
with open(fileName, mode="w+") as outFile:
|
with open(fileName, mode="w+", encoding="utf-8") as outFile:
|
||||||
outFile.write(writeText)
|
outFile.write(writeText)
|
||||||
|
|
||||||
|
|
||||||
def toUpload(srcPath, dstName=None):
|
def toUpload(srcPath, dstName=None):
|
||||||
"""Copy a file produced by one of the build functions to the uplaod
|
"""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"
|
uplDir = "dist_upload"
|
||||||
if not os.path.isdir(uplDir):
|
if not os.path.isdir(uplDir):
|
||||||
os.mkdir(uplDir)
|
os.mkdir(uplDir)
|
||||||
@@ -127,7 +126,7 @@ def makeCheckSum(sumFile, cwd=None):
|
|||||||
else:
|
else:
|
||||||
shaFile = os.path.join(cwd, sumFile+".sha256")
|
shaFile = os.path.join(cwd, sumFile+".sha256")
|
||||||
with open(shaFile, mode="w") as fOut:
|
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)
|
print("SHA256 Sum: %s" % shaFile)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print("Could not generate sha256 file")
|
print("Could not generate sha256 file")
|
||||||
@@ -331,6 +330,12 @@ def buildQtI18nTS(sysArgs):
|
|||||||
print("Building Qt Translation Files")
|
print("Building Qt Translation Files")
|
||||||
print("=============================")
|
print("=============================")
|
||||||
|
|
||||||
|
try:
|
||||||
|
from PyQt6.lupdate import lupdate
|
||||||
|
except ImportError:
|
||||||
|
print("ERROR: This command requires lupdate from PyQt6")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
print("")
|
print("")
|
||||||
print("Scanning Source Tree:")
|
print("Scanning Source Tree:")
|
||||||
print("")
|
print("")
|
||||||
@@ -386,10 +391,7 @@ def buildQtI18nTS(sysArgs):
|
|||||||
print("Updating Language Files:")
|
print("Updating Language Files:")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
# Using the pylupdate tool from PyQt6 instead as it supports TS file
|
# Using the pylupdate tool from PyQt6 as it supports TS file format 2.1.
|
||||||
# format 2.1. This can perhaps be changed back to the installed tool
|
|
||||||
# at a later time.
|
|
||||||
from i18n.pylupdate6 import lupdate
|
|
||||||
lupdate(srcList, tsList, no_obsolete=True, no_summary=False)
|
lupdate(srcList, tsList, no_obsolete=True, no_summary=False)
|
||||||
|
|
||||||
print("")
|
print("")
|
||||||
@@ -402,7 +404,7 @@ def buildQtI18nTS(sysArgs):
|
|||||||
##
|
##
|
||||||
|
|
||||||
def genMacOSPlist():
|
def genMacOSPlist():
|
||||||
"""Set necessary values for .plist file.
|
"""Set necessary values for .plist file for MacOS build.
|
||||||
"""
|
"""
|
||||||
numVers, _, _ = extractVersion()
|
numVers, _, _ = extractVersion()
|
||||||
pkgVers = compactVersion(numVers)
|
pkgVers = compactVersion(numVers)
|
||||||
@@ -936,11 +938,10 @@ def makeForLaunchpad(doSign=False, isFirst=False, isSnapshot=False):
|
|||||||
##
|
##
|
||||||
|
|
||||||
def makeAppImage(sysArgs):
|
def makeAppImage(sysArgs):
|
||||||
"""Build an Appimage.
|
"""Build an AppImage.
|
||||||
"""
|
"""
|
||||||
import glob
|
import glob
|
||||||
import argparse
|
import argparse
|
||||||
import platform
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import python_appimage # noqa F401
|
import python_appimage # noqa F401
|
||||||
@@ -964,15 +965,15 @@ def makeAppImage(sysArgs):
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--linux-tag",
|
"--linux-tag",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
default=f"manylinux2010_{platform.machine()}",
|
default="manylinux_2_28_x86_64",
|
||||||
help=(
|
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"
|
"see https://python-appimage.readthedocs.io/en/latest/#available-python-appimages \n"
|
||||||
"and https://github.com/pypa/manylinux for a list of valid tags"
|
"and https://github.com/pypa/manylinux for a list of valid tags"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
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)
|
args, unparsedArgs = parser.parse_known_args(sysArgs)
|
||||||
@@ -1016,9 +1017,8 @@ def makeAppImage(sysArgs):
|
|||||||
|
|
||||||
os.mkdir(imageDir)
|
os.mkdir(imageDir)
|
||||||
|
|
||||||
# Remove old Appimages
|
# Remove old AppImages
|
||||||
outFiles = glob.glob(f"{bldDir}/*.AppImage")
|
outFiles = glob.glob(f"{bldDir}/*.AppImage")
|
||||||
|
|
||||||
if outFiles:
|
if outFiles:
|
||||||
print("Removing old AppImages")
|
print("Removing old AppImages")
|
||||||
print("")
|
print("")
|
||||||
@@ -1108,17 +1108,17 @@ def makeAppImage(sysArgs):
|
|||||||
print("Wrote: requirements.txt")
|
print("Wrote: requirements.txt")
|
||||||
|
|
||||||
shutil.copyfile("setup/data/novelwriter.desktop", f"{imageDir}/novelwriter.desktop")
|
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")
|
shutil.copyfile("setup/icons/novelwriter.svg", f"{imageDir}/novelwriter.svg")
|
||||||
print("Copied: setup/icons/novelwriter.svg")
|
print("Copied: novelwriter.svg")
|
||||||
|
|
||||||
shutil.copyfile(
|
shutil.copyfile(
|
||||||
"setup/data/hicolor/256x256/apps/novelwriter.png", f"{imageDir}/novelwriter.png"
|
"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:
|
try:
|
||||||
@@ -1131,13 +1131,17 @@ def makeAppImage(sysArgs):
|
|||||||
print("")
|
print("")
|
||||||
print(str(exc))
|
print(str(exc))
|
||||||
print("")
|
print("")
|
||||||
print("Dependencies:")
|
|
||||||
print(" * pip install python-appimage")
|
|
||||||
print("")
|
|
||||||
sys.exit(1)
|
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]
|
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)
|
os.rename(bldFile, outFile)
|
||||||
shaFile = makeCheckSum(os.path.basename(outFile), cwd=bldDir)
|
shaFile = makeCheckSum(os.path.basename(outFile), cwd=bldDir)
|
||||||
|
|
||||||
@@ -1368,11 +1372,8 @@ def makeWindowsEmbedded(sysArgs):
|
|||||||
print(str(exc))
|
print(str(exc))
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
issName = os.path.join("dist", f"novelwriter-{packVersion}-win10-amd64-setup.exe")
|
print("")
|
||||||
newName = os.path.join("dist", f"novelwriter-{packVersion}-py{pyVers}-win10-amd64-setup.exe")
|
print("Done")
|
||||||
os.replace(issName, newName)
|
|
||||||
|
|
||||||
print(f"Installer: {newName}")
|
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -1904,13 +1905,11 @@ if __name__ == "__main__":
|
|||||||
" build-win-exe Build a setup.exe file with Python embedded for Windows.",
|
" build-win-exe Build a setup.exe file with Python embedded for Windows.",
|
||||||
" The package must be built from a minimal windows zip file.",
|
" The package must be built from a minimal windows zip file.",
|
||||||
" build-appimage Build an AppImage. Argument --linux-tag defaults to",
|
" 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:",
|
"System Install:",
|
||||||
"",
|
"",
|
||||||
" install Installs novelWriter to the system's Python install",
|
" install Deprecated. Use pip or build.",
|
||||||
" location. Run as root or with sudo for system-wide install,",
|
|
||||||
" or as user for single user install.",
|
|
||||||
" xdg-install Install launcher and icons for freedesktop systems. Run as",
|
" xdg-install Install launcher and icons for freedesktop systems. Run as",
|
||||||
" root or with sudo for system-wide install, or as user for",
|
" root or with sudo for system-wide install, or as user for",
|
||||||
" single user install.",
|
" single user install.",
|
||||||
@@ -2063,6 +2062,14 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# Run the standard setup
|
# Run the standard setup
|
||||||
import setuptools # noqa: F401
|
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()
|
setuptools.setup()
|
||||||
|
|
||||||
# END Main
|
# 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
|
Source: https://github.com/vkbo/novelWriter
|
||||||
|
|
||||||
Files: *
|
Files: *
|
||||||
Copyright: 2018-2021, Veronica Berglyd Olsen
|
Copyright: 2018-2023, Veronica Berglyd Olsen
|
||||||
License: GPL-3.0-or-later
|
License: GPL-3.0-or-later
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
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
|
You should have received a copy of the GNU General Public License
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
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
|
Files: novelwriter/assets/icons/typicons_*.svg
|
||||||
Copyright: 2019, Stephen Hutchings
|
Copyright: 2019, Stephen Hutchings
|
||||||
License: CC-BY-SA-4.0
|
License: CC-BY-SA-4.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
novelWriter License
|
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>
|
License: GPL v3+ <https://www.gnu.org/licenses/gpl-3.0.html>
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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/../.."
|
SRC_DIR="$SCRIPT_DIR/../.."
|
||||||
RLS_DIR="$SRC_DIR/dist_macos"
|
RLS_DIR="$SRC_DIR/dist_macos"
|
||||||
|
|
||||||
echo "Script Dir: $SCRIPT_DIR"
|
|
||||||
|
|
||||||
cleanup () {
|
cleanup () {
|
||||||
if [ -d "$BUILD_DIR" ]; then
|
if [ -d "$BUILD_DIR" ]; then
|
||||||
rm -rf "$BUILD_DIR"
|
rm -rf "$BUILD_DIR"
|
||||||
@@ -21,13 +19,14 @@ cleanup () {
|
|||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
echo "Building in: $BUILD_DIR"
|
echo "Script Dir: $SCRIPT_DIR"
|
||||||
|
echo "Build Dir: $BUILD_DIR"
|
||||||
OLD_CWD="$(pwd)"
|
|
||||||
VERSION="$(awk '/^__version__/{print substr($NF,2,length($NF)-2)}' $SRC_DIR/novelwriter/__init__.py)"
|
|
||||||
|
|
||||||
pushd "$SRC_DIR" || exit 1
|
pushd "$SRC_DIR" || exit 1
|
||||||
|
|
||||||
|
VERSION="$(python3 setup.py version)"
|
||||||
|
echo "novelWriter Version: $VERSION"
|
||||||
|
|
||||||
# --- Prepare Files ----------------------------------------------------------------------------- #
|
# --- Prepare Files ----------------------------------------------------------------------------- #
|
||||||
|
|
||||||
echo "Generating Info.plist"
|
echo "Generating Info.plist"
|
||||||
@@ -68,23 +67,21 @@ pushd "$BUILD_DIR"/ || exit 1
|
|||||||
|
|
||||||
# --- Create Miniconda Env ---------------------------------------------------------------------- #
|
# --- Create Miniconda Env ---------------------------------------------------------------------- #
|
||||||
|
|
||||||
# install Miniconda, a self-contained Python distribution
|
|
||||||
echo "Downloading Miniconda ..."
|
echo "Downloading Miniconda ..."
|
||||||
curl -LO https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh
|
curl -LO https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh
|
||||||
bash Miniconda3-latest-MacOSX-x86_64.sh -b -p ~/miniconda -f
|
bash Miniconda3-latest-MacOSX-x86_64.sh -b -p ~/miniconda -f
|
||||||
rm Miniconda3-latest-MacOSX-x86_64.sh
|
rm Miniconda3-latest-MacOSX-x86_64.sh
|
||||||
export PATH="$HOME/miniconda/bin:$PATH"
|
export PATH="$HOME/miniconda/bin:$PATH"
|
||||||
|
|
||||||
echo "Creating conda env ..."
|
echo "Creating Conda env ..."
|
||||||
# create conda env
|
|
||||||
conda create -n novelWriter -c conda-forge python=3.10 --yes
|
conda create -n novelWriter -c conda-forge python=3.10 --yes
|
||||||
source activate novelWriter
|
source activate novelWriter
|
||||||
|
|
||||||
echo "installing dictionaries ..."
|
echo "Installing dictionaries ..."
|
||||||
conda install -c conda-forge enchant hunspell-en --yes
|
conda install -c conda-forge enchant hunspell-en --yes
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
echo "installing python dependencies ..."
|
echo "Installing Python dependencies ..."
|
||||||
pip install -r "$SRC_DIR/requirements.txt"
|
pip install -r "$SRC_DIR/requirements.txt"
|
||||||
|
|
||||||
# Leave conda env
|
# Leave conda env
|
||||||
@@ -92,8 +89,9 @@ conda deactivate
|
|||||||
|
|
||||||
# --- Build App --------------------------------------------------------------------------------- #
|
# --- Build App --------------------------------------------------------------------------------- #
|
||||||
|
|
||||||
echo "Building app bundle ..."
|
echo "Building App bundle ..."
|
||||||
# create .app Framework
|
|
||||||
|
# Create .app Framework
|
||||||
mkdir -p novelWriter.app/Contents/
|
mkdir -p novelWriter.app/Contents/
|
||||||
mkdir novelWriter.app/Contents/MacOS novelWriter.app/Contents/Resources novelWriter.app/Contents/Resources/novelWriter
|
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
|
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 ..."
|
# echo "Signing bundle ..."
|
||||||
# sudo codesign --sign - --deep --force --entitlements "$SCRIPT_DIR/../macos/App.entitlements" --options runtime "novelWriter.app/Contents/MacOS/novelWriter"
|
# 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 ----------------------------------------------------------------------------------- #
|
# --- Cleanup ----------------------------------------------------------------------------------- #
|
||||||
|
|
||||||
echo "Cleaning unused files from bundle ..."
|
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
|
find . -type d -iname '__pycache__' -print0 | xargs -0 rm -r
|
||||||
|
|
||||||
rm -rf pkgs
|
rm -rf pkgs
|
||||||
@@ -146,7 +142,7 @@ rm -rf share/{gtk-,}doc
|
|||||||
# Remove the files from the 3.1 symlink
|
# Remove the files from the 3.1 symlink
|
||||||
rm -rf lib/python3.1
|
rm -rf lib/python3.1
|
||||||
|
|
||||||
# remove web engine
|
# Remove web engine
|
||||||
rm lib/python3.*/site-packages/PyQt5/QtWebEngine* || true
|
rm lib/python3.*/site-packages/PyQt5/QtWebEngine* || true
|
||||||
rm -r lib/python3.*/site-packages/PyQt5/Qt/translations/qtwebengine* || true
|
rm -r lib/python3.*/site-packages/PyQt5/Qt/translations/qtwebengine* || true
|
||||||
rm lib/python3.*/site-packages/PyQt5/Qt/resources/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
|
||||||
popd || exit 1
|
popd || exit 1
|
||||||
|
|
||||||
# --- Create App Bundle-------------------------------------------------------------------------- #
|
|
||||||
|
|
||||||
echo "Packageing App ..."
|
|
||||||
mkdir -p $RLS_DIR
|
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 -------------------------------------------------------------------------------- #
|
# --- Create DMG -------------------------------------------------------------------------------- #
|
||||||
|
|
||||||
# Generate .dmg
|
# Generate .dmg
|
||||||
echo "Packageing DMG ..."
|
echo "Packageing DMG ..."
|
||||||
brew install create-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 \
|
create-dmg --volname "novelWriter $VERSION" --volicon $SRC_DIR/setup/macos/novelwriter.icns \
|
||||||
--window-pos 200 120 --window-size 800 400 --icon-size 100 \
|
--window-pos 200 120 --window-size 800 400 --icon-size 100 \
|
||||||
--icon novelWriter.app 200 190 --hide-extension novelWriter.app \
|
--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 "================================================================================"
|
||||||
echo ""
|
echo ""
|
||||||
python3 setup.py qtlrelease manual sample
|
python3 setup.py qtlrelease manual sample
|
||||||
python3 setup.py sdist bdist_wheel
|
python3 -m build
|
||||||
mkdir -pv dist_upload
|
mkdir -pv dist_upload
|
||||||
cp -v dist/novelWriter-*.whl 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 ""
|
||||||
echo " Checking Packages"
|
echo " Checking Packages"
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ DisableProgramGroupPage=yes
|
|||||||
UsedUserAreasWarning=no
|
UsedUserAreasWarning=no
|
||||||
PrivilegesRequiredOverridesAllowed=dialog
|
PrivilegesRequiredOverridesAllowed=dialog
|
||||||
OutputDir={#nwAppDir}
|
OutputDir={#nwAppDir}
|
||||||
OutputBaseFilename=novelwriter-{#nwAppVersion}-win10-amd64-setup
|
OutputBaseFilename=novelwriter-{#nwAppVersion}-amd64-setup
|
||||||
Compression=lzma
|
Compression=lzma
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
|
|||||||
Reference in New Issue
Block a user