diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml new file mode 100644 index 0000000..2130133 --- /dev/null +++ b/.github/workflows/build_windows.yml @@ -0,0 +1,59 @@ +# This workflow is used to run the pyinstaller spec +# and produce a Windows build directory as artifact. + +# In the future it'll also need to confirm that all +# of the translations and UI files are built from +# scratch as a condition of the tests with pytest. + +name: Build for Windows + +# Controls when the action will run. +on: + # Triggers the workflow on push or pull request events but only for the develop branch + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + windows_build: + name: Building for Windows ${{ matrix.python_version }} on ${{ matrix.os }} + # The type of runner that the job will run on + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.9] + os: [windows-latest] + steps: + # We use the version from this commit and not just the current branch. This is for R&D builds too. + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + # These should be moved to a pyproject.toml file down the line. + - name: Install Python build dependencies + run: | + python -m pip install --upgrade pip + pip install pyqt5==5.15.7 lxml pytest pytest-faulthandler language_tool_python symspellpy pyspellchecker pyenchant + pip install pyinstaller + - name: pyinstaller build + run: | + pyinstaller ./manuskript.spec + env: + manuskript_version: ${{ steps.vars.outputs.sha_short }} + - name: Remove W7 File Issues # https://github.com/olivierkes/manuskript/blob/develop/package/build_for_windows.sh#L85 + run: | + powershell Remove-Item ./dist/manuskript/PyQt5/Qt5/bin/Qt5Bluetooth.dll; + powershell Remove-Item ./dist/manuskript/ucrtbase.dll; + powershell Remove-Item ./dist/manuskript/api-ms-win-*; + + # https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts + - name: Upload Artifacts + uses: actions/upload-artifact@v3 + with: + name: manuskript_windows + path: ./dist/manuskript/ diff --git a/.github/workflows/linux.yml b/.github/workflows/pytest.yml similarity index 64% rename from .github/workflows/linux.yml rename to .github/workflows/pytest.yml index 7286cb9..1900d33 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/pytest.yml @@ -1,6 +1,7 @@ -# This is a basic workflow to help you get started with Actions +# This workflow is used to run pytest and confirm +# that Linux builds meet the pytest requirements for the project. -name: CI +name: Pytest Run (Linux) # Controls when the action will run. on: @@ -13,31 +14,28 @@ on: # Allows you to run this workflow manually from the Actions tab workflow_dispatch: -# A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "build" test: - name: Test on node ${{ matrix.python_version }} and ${{ matrix.os }} + name: Running pytest ${{ matrix.python_version }} and ${{ matrix.os }} # The type of runner that the job will run on runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.6] - # python-version: [3.6, 3.7, 3.8, 3.9] - os: [ubuntu-16.04] - # os: [ubuntu-16.04, ubuntu-latest, windows-latest, macos-10.15] + # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs + python-version: [3.9] + os: [ubuntu-20.04] # Steps represent a sequence of tasks that will be executed as part of the job steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pyqt5==5.9 lxml pytest pytest-faulthandler + pip install pyqt5==5.15.7 lxml pytest pytest-faulthandler sudo apt-get -qq update sudo apt-get -qq install python3-pip python3-dev build-essential qt5-default libxml2-dev libxslt1-dev mesa-utils libgl1-mesa-glx libgl1-mesa-dev libxcb-xinerama0-dev if [ -f requirements.txt ]; then pip install -r requirements.txt; fi diff --git a/manuskript.spec b/manuskript.spec index fa119ef..91b1361 100644 --- a/manuskript.spec +++ b/manuskript.spec @@ -2,80 +2,98 @@ block_cipher = None -a = Analysis(['bin/manuskript'], - pathex=['.'], - binaries=None, - datas=[ - ("icons", "icons"), - ("libs", "libs"), - ("resources", "resources"), - ("sample-projects", "sample-projects"), - ("i18n", "i18n"), - ], - hiddenimports=["xml.dom"], - hookspath=[], - runtime_hooks=[], - excludes=[], - win_no_prefer_redirects=False, - win_private_assemblies=False, - cipher=block_cipher) +# This is the spec file to be run with pyinstaller. +# However, it's what amounts to a python script, so +# we can do some incredibly cursed things to make +# sure it builds, and the way we want it to. -pyz = PYZ(a.pure, a.zipped_data, - cipher=block_cipher) +# For some reason we need to explicitly include the current directory. Unsure why. +import os +import sys +file_dir = os.path.dirname(".") +sys.path.append(file_dir) -exe = EXE(pyz, - a.scripts, - exclude_binaries=True, - name='manuskript', - debug=False, - strip=False, - upx=True, - console=True, - icon=os.path.join(SPECPATH, 'icons/Manuskript/manuskript.ico') ) +# We're grabbing the current git SHA short to use in the version. -wexe = EXE(pyz, - a.scripts, - exclude_binaries=True, - name='manuskriptw', - debug=False, - strip=False, - upx=True, - console=False, - icon=os.path.join(SPECPATH, 'icons/Manuskript/manuskript.ico') ) +from util.hashed_version import writeVersionPlusHash -coll = COLLECT(exe, - wexe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - name='manuskript') +version = writeVersionPlusHash() -version=os.environ['manuskript_version'] +a = Analysis( + ["bin/manuskript"], + pathex=["."], + binaries=None, + datas=[ + ("icons", "icons"), + ("libs", "libs"), + ("resources", "resources"), + ("sample-projects", "sample-projects"), + ("i18n", "i18n"), + ], + hiddenimports=["xml.dom"], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, +) -app = BUNDLE(coll, - name='manuskript.app', - icon=os.path.join(SPECPATH, 'icons/Manuskript/Manuskript.icns'), - bundle_identifier='ch.theologeek.manuskript', - version=version, - info_plist={ - 'NSPrincipalClass': 'NSApplication', - 'NSAppleScriptEnabled': False, - 'NSHighResolutionCapable': True, - 'CFBundleURLTypes': [{ - 'CFBundleURLName': 'MSK', - 'CFBundleTypeRole': 'Editor', - 'CFBundleURLSchemes': ['msk'], - }], - 'CFBundleDocumentTypes': [ - { - 'CFBundleTypeName': 'MSK', - 'CFBundleTypeIconFile': 'icons/Manuscript/manuskript', - 'CFBundleTypeExtensions': ['msk'], - 'CFBundleTypeRole': 'Editor', - 'LSHandlerRank': 'Owner' - } - ] - }, - ) \ No newline at end of file +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + exclude_binaries=True, + name="manuskript", + debug=False, + strip=False, + upx=True, + console=True, + icon=os.path.join(SPECPATH, "icons/Manuskript/manuskript.ico"), +) + +wexe = EXE( + pyz, + a.scripts, + exclude_binaries=True, + name="manuskriptw", + debug=False, + strip=False, + upx=True, + console=False, + icon=os.path.join(SPECPATH, "icons/Manuskript/manuskript.ico"), +) + +coll = COLLECT( + exe, wexe, a.binaries, a.zipfiles, a.datas, strip=False, upx=True, name="manuskript" +) + +app = BUNDLE( + coll, + name="manuskript.app", + icon=os.path.join(SPECPATH, "icons/Manuskript/Manuskript.icns"), + bundle_identifier="ch.theologeek.manuskript", + version=version, + info_plist={ + "NSPrincipalClass": "NSApplication", + "NSAppleScriptEnabled": False, + "NSHighResolutionCapable": True, + "CFBundleURLTypes": [ + { + "CFBundleURLName": "MSK", + "CFBundleTypeRole": "Editor", + "CFBundleURLSchemes": ["msk"], + } + ], + "CFBundleDocumentTypes": [ + { + "CFBundleTypeName": "MSK", + "CFBundleTypeIconFile": "icons/Manuscript/manuskript", + "CFBundleTypeExtensions": ["msk"], + "CFBundleTypeRole": "Editor", + "LSHandlerRank": "Owner", + } + ], + }, +) diff --git a/manuskript/load_save/version_1.py b/manuskript/load_save/version_1.py index b2ad66c..302f790 100644 --- a/manuskript/load_save/version_1.py +++ b/manuskript/load_save/version_1.py @@ -964,9 +964,9 @@ def addTextItems(mdl, odict, parent=None): item = outlineFromMMD(odict[k], parent=parent) item._lastPath = odict[k + ":lastPath"] except KeyError: - LOGGER.error("Failed to add file " + str(k)) + LOGGER.error(f"Failed to add file {k}") else: - LOGGER.debug("Strange things in file %s".format(k)) + LOGGER.debug(f"Strange things in file {k}") def outlineFromMMD(text, parent): diff --git a/util/__init__.py b/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/util/fix_ts.py b/util/fix_ts.py index 388cbcc..ae6fc97 100644 --- a/util/fix_ts.py +++ b/util/fix_ts.py @@ -5,6 +5,7 @@ import sys from lxml import etree + def main(argv) -> int: if len(argv) < 2: print("You need to specify a .ts file!") @@ -12,13 +13,13 @@ def main(argv) -> int: path = argv[1] - if (len(path) < 3) or (path[-3:] != '.ts'): + if (len(path) < 3) or (path[-3:] != ".ts"): print("Please specify a path to a .ts file!") return 2 tree = None - with open(path, 'rb') as file: + with open(path, "rb") as file: tree = etree.parse(file) if tree is None: @@ -26,16 +27,16 @@ def main(argv) -> int: return 3 root = tree.getroot() - if root.tag != 'TS': + if root.tag != "TS": print("Wrong type of file!") return 4 for context in root.getchildren(): - if context.tag != 'context': + if context.tag != "context": continue for message in context.getchildren(): - if message.tag != 'message': + if message.tag != "message": continue source = message.find("source") @@ -44,22 +45,23 @@ def main(argv) -> int: if (source is None) or (translation is None): continue - sourceText = etree.tostring(source, encoding='unicode').strip() + sourceText = etree.tostring(source, encoding="unicode").strip() - if '&' in sourceText: + if "&" in sourceText: continue - translationText = etree.tostring(translation, encoding='unicode').strip() - translationText = re.sub(r'&([a-zA-Z]+);', r'&\g<1>;', translationText) + translationText = etree.tostring(translation, encoding="unicode").strip() + translationText = re.sub(r"&([a-zA-Z]+);", r"&\g<1>;", translationText) translationNode = etree.fromstring(translationText) translation.text = translationNode.text - with open(path, 'wb') as file: - tree.write(file, encoding='utf-8', xml_declaration=True, pretty_print=True) + with open(path, "wb") as file: + tree.write(file, encoding="utf-8", xml_declaration=True, pretty_print=True) print("Fixing finished!") return 0 -if __name__ == '__main__': + +if __name__ == "__main__": sys.exit(main(sys.argv)) diff --git a/util/hashed_version.py b/util/hashed_version.py new file mode 100644 index 0000000..25d89fb --- /dev/null +++ b/util/hashed_version.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import subprocess +import pathlib +import importlib.util +import sys + +# This is to produce an environment variable in GH Actions exclusively for use in CI/CD builds. + + +def writeVersionPlusHash() -> bool: + """This function will permit us to rewrite + the inline version.py file to contain the + appropriate version-plus-hash-short to be + shown in the about screen, version diagnostics + and more. This helps us better keep track + of long-running issues in a relatively + consise manner.""" + + sha_short = ( + subprocess.run( + "git rev-parse --short HEAD", shell=True, check=True, capture_output=True + ) + .stdout.strip() + .decode("utf-8") + ) # UTF8 decode as it's a UTF8 situation. + + + version_file = pathlib.Path("./manuskript/version.py").absolute() + + spec = importlib.util.spec_from_file_location( + "manuskript.version", version_file.absolute() + ) + temp = importlib.util.module_from_spec(spec) + sys.modules["module.name"] = temp + spec.loader.exec_module(temp) + + # used in final output. + non_commit_version = temp.getVersion() + + # Used to rewrite the situation here. + + version_file_contents = open(str(version_file), 'r').readlines() + + for key, f in enumerate(version_file_contents): + if ("__version__ = ") in f: + version_file_contents[key] = f"__version__ = \"{non_commit_version}-{sha_short}\"\n" + + try: + with open(str(version_file), 'w') as output_stub: + output_stub.writelines(version_file_contents) + return f"{non_commit_version}-{sha_short}" + except: + return None + \ No newline at end of file diff --git a/util/linguist_update.py b/util/linguist_update.py new file mode 100644 index 0000000..93705ca --- /dev/null +++ b/util/linguist_update.py @@ -0,0 +1,44 @@ +#!/bin/python + +import argparse +import os +from pathlib import Path +import subprocess + +parser = argparse.ArgumentParser( + description="Update all QM source translation binaries based on the source .ts files." +) +parser.add_argument( + "--binpath", type=str, help="Path to the lrelease binary.", required=True +) +parser.add_argument( + "--transpath", + type=str, + help="Path to the source translations directory. Does not recurse.", + default="./i18n/", +) + + +def cleanupFiles(binpath, transpath) -> None: + # Catch for if the user doesn't pass in a path but we're still passing a None. + src_files = Path(transpath) if transpath is not None else "./i18n/" + src_files = [ + x for x in src_files.iterdir() if x.is_file() and str(x).endswith(".qm") + ] + + for file in src_files: + # lrelease.exe path_to_translation.qm + subprocess.run([lrelease_path, str(file)]) + + +# This permits us to import these steps discretely for a bigger build tool. +if __name__ == "__main__": + arguments = parser.parse_args() + + # To permit calling as a normal function too ig. + if arguments.binpath is not None: + lrelease_path = Path(arguments.binpath) + if arguments.transpath is not None: + trans_path = Path(arguments.transpath) + + cleanupFiles(lrelease_path, trans_path)