From 3a7907c8dae9df66904adc212e45a3d3ac8b18c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9-Abush=20CLAUSE?= Date: Tue, 15 Aug 2023 15:58:54 +0200 Subject: [PATCH] Update Addon Template --- .github/workflows/build_addon.yml | 64 ++++++++ .pre-commit-config.yaml | 7 + buildVars.py | 67 +++++--- sconstruct | 244 +++++++++++++++++++++++------- style.css | 4 - 5 files changed, 308 insertions(+), 78 deletions(-) create mode 100644 .github/workflows/build_addon.yml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml new file mode 100644 index 00000000..9b97cd9f --- /dev/null +++ b/.github/workflows/build_addon.yml @@ -0,0 +1,64 @@ +name: build addon + +on: + push: + tags: ["*"] + # To build on main/master branch, uncomment the following line: + # branches: [ main , master ] + + pull_request: + branches: [ main, master ] + + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - run: echo -e "pre-commit\nscons\nmarkdown">requirements.txt + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.9 + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip wheel + pip install -r requirements.txt + sudo apt-get update -y + sudo apt-get install -y gettext + + - name: Code checks + run: export SKIP=no-commit-to-branch; pre-commit run --all + + - name: building addon + run: scons + + - uses: actions/upload-artifact@v3 + with: + name: packaged_addon + path: ./*.nvda-addon + + upload_release: + runs-on: ubuntu-latest + if: ${{ startsWith(github.ref, 'refs/tags/') }} + needs: ["build"] + steps: + - uses: actions/checkout@v3 + - name: download releases files + uses: actions/download-artifact@v3 + - name: Display structure of downloaded files + run: ls -R + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: packaged_addon/*.nvda-addon + fail_on_unmatched_files: true + prerelease: ${{ contains(github.ref, '-') }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..dd7a9d69 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-ast + - id: check-case-conflict + - id: check-yaml diff --git a/buildVars.py b/buildVars.py index 8b27f9e2..b5ef1fcb 100644 --- a/buildVars.py +++ b/buildVars.py @@ -1,8 +1,19 @@ -# coding: utf-8 -import os.path +# -*- coding: UTF-8 -*- import subprocess import time +# Build customizations +# Change this file instead of sconstruct or manifest files, whenever possible. + + +# Since some strings in `addon_info` are translatable, +# we need to include them in the .po files. +# Gettext recognizes only strings given as parameters to the `_` function. +# To avoid initializing translations in this module we simply roll our own "fake" `_` function +# which returns whatever is given to it as an argument. +def _(arg): + return arg + updateChannel = "unknown" hashCommit = "unknown" outBranchName = subprocess.check_output( @@ -17,23 +28,14 @@ if outBranchName.strip(): updateChannel = "stable" if outBranchName in [ "stable", "master"] else "dev" -# Build customizations -# Change this file instead of sconstruct or manifest files, whenever possible. - -# Full getext (please don't change) - - -def _(x): return x - # Add-on information variables addon_info = { - # for previously unpublished addons, please follow the community guidelines at: - # https://bitbucket.org/nvdaaddonteam/todo/raw/master/guidelines.txt - # add-on Name, internal for nvda + # add-on Name/identifier, internal for NVDA "addon_name": "BrailleExtender", # Add-on summary, usually the user visible name of the addon. - # Translators: Summary for this add-on to be shown on installation and add-on information. + # Translators: Summary for this add-on + # to be shown on installation and add-on information found in Add-ons Manager. "addon_summary": _("Braille Extender"), # Add-on description # Translators: Long description to be shown for this add-on on add-on information from add-ons manager @@ -71,23 +73,34 @@ def _(x): return x # Author(s) "addon_author": "André-Abush Clause " + _("and other contributors"), # URL for the add-on documentation support - "addon_url": "https://andreabc.net/projects/NVDA_addons/BrailleExtender", + "addon_url": "https://github.com/aaclause/brailleExtender/", + # URL for the add-on repository where the source code can be found + "addon_sourceURL": "https://github.com/aaclause/brailleExtender/", # Documentation file name "addon_docFileName": "readme.html", # Minimum NVDA version supported (e.g. "2018.3.0", minor version is optional) "addon_minimumNVDAVersion": "2021.1", # Last NVDA version supported/tested (e.g. "2018.4.0", ideally more recent than minimum version) "addon_lastTestedNVDAVersion": "2023.1", - # Add-on update channel (default is stable or None) + # Add-on update channel (default is None, denoting stable releases, + # and for development releases, use "dev".) + # Do not change unless you know what you are doing! "addon_updateChannel": updateChannel, + # Add-on license such as GPL 2 + "addon_license": "GPL v2", + # URL for the license document the ad-on is licensed under + "addon_licenseURL": "https://www.gnu.org/licenses/gpl-2.0.html", } - # Define the python files that are the sources of your add-on. -# You can use glob expressions here, they will be expanded. -pythonSources = [os.path.join("addon", "*.py"), - os.path.join("addon", "globalPlugins", "brailleExtender", "*.py")] - +# You can either list every file (using ""/") as a path separator, +# or use glob expressions. +# For example to include all files with a ".py" extension from the "globalPlugins" dir of your add-on +# the list can be written as follows: +# pythonSources = ["addon/globalPlugins/*.py"] +# For more information on SCons Glob expressions please take a look at: +# https://scons.org/doc/production/HTML/scons-user/apd.html +pythonSources = [] # Files that contain strings for translation. Usually your python sources i18nSources = pythonSources + ["buildVars.py"] @@ -95,3 +108,15 @@ def _(x): return x # Files that will be ignored when building the nvda-addon file # Paths are relative to the addon directory, not to the root directory of your addon sources. excludedFiles = [] + +# Base language for the NVDA add-on +# If your add-on is written in a language other than english, modify this variable. +# For example, set baseLanguage to "es" if your add-on is primarily written in spanish. +baseLanguage = "en" + +# Markdown extensions for add-on documentation +# Most add-ons do not require additional Markdown extensions. +# If you need to add support for markup such as tables, fill out the below list. +# Extensions string must be of the form "markdown.extensions.extensionName" +# e.g. "markdown.extensions.tables" to add tables. +markdownExtensions = [] diff --git a/sconstruct b/sconstruct index de86b7ad..63ef8238 100644 --- a/sconstruct +++ b/sconstruct @@ -1,20 +1,41 @@ # NVDA add-on template SCONSTRUCT file -#Copyright (C) 2012, 2014 Rui Batista -#This file is covered by the GNU General Public License. -#See the file COPYING.txt for more details. +# Copyright (C) 2012-2023 Rui Batista, Noelia Martinez, Joseph Lee +# This file is covered by the GNU General Public License. +# See the file COPYING.txt for more details. import codecs import gettext import os import os.path import zipfile +import sys + +# While names imported below are available by default in every SConscript +# Linters aren't aware about them. +# To avoid Flake8 F821 warnings about them they are imported explicitly. +# When using other Scons functions please add them to the line below. +from SCons.Script import BoolVariable, Builder, Copy, Environment, Variables + +sys.dont_write_bytecode = True + +# Bytecode should not be written for build vars module to keep the repository root folder clean. +import buildVars # NOQA: E402 -import buildVars def md2html(source, dest): import markdown + # Use extensions if defined. + mdExtensions = buildVars.markdownExtensions lang = os.path.basename(os.path.dirname(source)).replace('_', '-') - title="{addonSummary} {addonVersion}".format(addonSummary=buildVars.addon_info["addon_summary"], addonVersion=buildVars.addon_info["addon_version"]) + localeLang = os.path.basename(os.path.dirname(source)) + try: + _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang]).gettext + summary = _(buildVars.addon_info["addon_summary"]) + except Exception: + summary = buildVars.addon_info["addon_summary"] + title = "{addonSummary} {addonVersion}".format( + addonSummary=summary, addonVersion=buildVars.addon_info["addon_version"] + ) headerDic = { "[[!meta title=\"": "# ", "\"]]": " #", @@ -23,35 +44,52 @@ def md2html(source, dest): mdText = f.read() for k, v in headerDic.items(): mdText = mdText.replace(k, v, 1) - htmlText = markdown.markdown(mdText) + htmlText = markdown.markdown(mdText, extensions=mdExtensions) + # Optimization: build resulting HTML text in one go instead of writing parts separately. + docText = "\n".join([ + "", + "" % lang, + "", + "" + "", + "", + "%s" % title, + "\n", + htmlText, + "\n" + ]) with codecs.open(dest, "w", "utf-8") as f: - f.write("\n" + - "\n" + - "\n" % (lang, lang) + - "\n" + - "\n" + - "\n" + - "%s\n" % title + - "\n\n" - ) - f.write(htmlText) - f.write("\n\n") + f.write(docText) + def mdTool(env): - mdAction=env.Action( - lambda target,source,env: md2html(source[0].path, target[0].path), - lambda target,source,env: 'Generating %s'%target[0], + mdAction = env.Action( + lambda target, source, env: md2html(source[0].path, target[0].path), + lambda target, source, env: 'Generating % s' % target[0], ) - mdBuilder=env.Builder( + mdBuilder = env.Builder( action=mdAction, suffix='.html', src_suffix='.md', ) - env['BUILDERS']['markdown']=mdBuilder + env['BUILDERS']['markdown'] = mdBuilder + + +def validateVersionNumber(key, val, env): + # Used to make sure version major.minor.patch are integers to comply with NV Access add-on store. + # Ignore all this if version number is not specified, in which case json generator will validate this info. + if val == "0.0.0": + return + versionNumber = val.split(".") + if len(versionNumber) < 3: + raise ValueError("versionNumber must have three parts (major.minor.patch)") + if not all([part.isnumeric() for part in versionNumber]): + raise ValueError("versionNumber (major.minor.patch) must be integers") + vars = Variables() vars.Add("version", "The version of this build", buildVars.addon_info["addon_version"]) +vars.Add("versionNumber", "Version number of the form major.minor.patch", "0.0.0", validateVersionNumber) vars.Add(BoolVariable("dev", "Whether this is a daily development version", False)) vars.Add("channel", "Update channel for this build", buildVars.addon_info["addon_updateChannel"]) @@ -62,36 +100,52 @@ if env["dev"]: import datetime buildDate = datetime.datetime.now() year, month, day = str(buildDate.year), str(buildDate.month), str(buildDate.day) - env["addon_version"] = "".join([year, month.zfill(2), day.zfill(2), "-dev"]) + versionTimestamp = "".join([year, month.zfill(2), day.zfill(2)]) + env["addon_version"] = f"{versionTimestamp}.0.0" + env["versionNumber"] = f"{versionTimestamp}.0.0" env["channel"] = "dev" elif env["version"] is not None: env["addon_version"] = env["version"] if "channel" in env and env["channel"] is not None: env["addon_updateChannel"] = env["channel"] -addonFile = env.File("${addon_name}-${addon_version.replace(':','-').strip()}.nvda-addon") +buildVars.addon_info["addon_version"] = env["addon_version"] +buildVars.addon_info["addon_updateChannel"] = env["addon_updateChannel"] + +addonFile = env.File("${addon_name}-${addon_version.replace(':', '-')}.nvda-addon") + def addonGenerator(target, source, env, for_signature): - action = env.Action(lambda target, source, env : createAddonBundleFromPath(source[0].abspath, target[0].abspath) and None, - lambda target, source, env : "Generating Addon %s" % target[0]) + action = env.Action( + lambda target, source, env: createAddonBundleFromPath(source[0].abspath, target[0].abspath) and None, + lambda target, source, env: "Generating Addon %s" % target[0] + ) return action + def manifestGenerator(target, source, env, for_signature): - action = env.Action(lambda target, source, env : generateManifest(source[0].abspath, target[0].abspath) and None, - lambda target, source, env : "Generating manifest %s" % target[0]) + action = env.Action( + lambda target, source, env: generateManifest(source[0].abspath, target[0].abspath) and None, + lambda target, source, env: "Generating manifest %s" % target[0] + ) return action + def translatedManifestGenerator(target, source, env, for_signature): dir = os.path.abspath(os.path.join(os.path.dirname(str(source[0])), "..")) lang = os.path.basename(dir) - action = env.Action(lambda target, source, env : generateTranslatedManifest(source[1].abspath, lang, target[0].abspath) and None, - lambda target, source, env : "Generating translated manifest %s" % target[0]) + action = env.Action( + lambda target, source, env: generateTranslatedManifest(source[1].abspath, lang, target[0].abspath) and None, + lambda target, source, env: "Generating translated manifest %s" % target[0] + ) return action + env['BUILDERS']['NVDAAddon'] = Builder(generator=addonGenerator) env['BUILDERS']['NVDAManifest'] = Builder(generator=manifestGenerator) env['BUILDERS']['NVDATranslatedManifest'] = Builder(generator=translatedManifestGenerator) + def createAddonHelp(dir): docsDir = os.path.join(dir, "doc") if os.path.isfile("style.css"): @@ -99,10 +153,11 @@ def createAddonHelp(dir): cssTarget = env.Command(cssPath, "style.css", Copy("$TARGET", "$SOURCE")) env.Depends(addon, cssTarget) if os.path.isfile("readme.md"): - readmePath = os.path.join(docsDir, "en", "readme.md") + readmePath = os.path.join(docsDir, buildVars.baseLanguage, "readme.md") readmeTarget = env.Command(readmePath, "readme.md", Copy("$TARGET", "$SOURCE")) env.Depends(addon, readmeTarget) + def createAddonBundleFromPath(path, dest): """ Creates a bundle from a directory that contains an addon manifest file.""" basedir = os.path.abspath(path) @@ -113,27 +168,98 @@ def createAddonBundleFromPath(path, dest): for filename in filenames: pathInBundle = os.path.join(relativePath, filename) absPath = os.path.join(dir, filename) - if pathInBundle not in buildVars.excludedFiles: z.write(absPath, pathInBundle) + if pathInBundle not in buildVars.excludedFiles: + z.write(absPath, pathInBundle) + createAddonStoreJson(dest) return dest + +def createAddonStoreJson(bundle): + """Creates add-on store JSON file from an add-on package and manifest data.""" + import json + import hashlib + # Set different json file names and version number properties based on version number parsing results. + if env["versionNumber"] == "0.0.0": + env["versionNumber"] = buildVars.addon_info["addon_version"] + versionNumberParsed = env["versionNumber"].split(".") + if all([part.isnumeric() for part in versionNumberParsed]): + if len(versionNumberParsed) == 1: + versionNumberParsed += ["0", "0"] + elif len(versionNumberParsed) == 2: + versionNumberParsed.append("0") + else: + versionNumberParsed = [] + if len(versionNumberParsed): + major, minor, patch = [int(part) for part in versionNumberParsed] + jsonFilename = f'{major}.{minor}.{patch}.json' + else: + jsonFilename = f'{buildVars.addon_info["addon_version"]}.json'.replace(':','-') + major, minor, patch = 0, 0, 0 + print('Generating % s' % jsonFilename) + sha256 = hashlib.sha256() + with open(bundle, "rb") as f: + for byte_block in iter(lambda: f.read(65536), b""): + sha256.update(byte_block) + hashValue = sha256.hexdigest() + try: + minimumNVDAVersion = buildVars.addon_info["addon_minimumNVDAVersion"].split(".") + except AttributeError: + minimumNVDAVersion = [0, 0, 0] + minMajor, minMinor = minimumNVDAVersion[:2] + minPatch = minimumNVDAVersion[-1] if len(minimumNVDAVersion) == 3 else "0" + try: + lastTestedNVDAVersion = buildVars.addon_info["addon_lastTestedNVDAVersion"].split(".") + except AttributeError: + lastTestedNVDAVersion = [0, 0, 0] + lastTestedMajor, lastTestedMinor = lastTestedNVDAVersion[:2] + lastTestedPatch = lastTestedNVDAVersion[-1] if len(lastTestedNVDAVersion) == 3 else "0" + channel = buildVars.addon_info["addon_updateChannel"] + if channel is None: + channel = "stable" + addonStoreEntry = { + "addonId": buildVars.addon_info["addon_name"], + "displayName": buildVars.addon_info["addon_summary"], + "URL": "", + "description": buildVars.addon_info["addon_description"], + "sha256": hashValue, + "homepage": buildVars.addon_info["addon_url"], + "addonVersionName": buildVars.addon_info["addon_version"], + "addonVersionNumber": { + "major": major, + "minor": minor, + "patch": patch + }, + "minNVDAVersion": { + "major": int(minMajor), + "minor": int(minMinor), + "patch": int(minPatch) + }, + "lastTestedVersion": { + "major": int(lastTestedMajor), + "minor": int(lastTestedMinor), + "patch": int(lastTestedPatch) + }, + "channel": channel, + "publisher": "", + "sourceURL": buildVars.addon_info["addon_sourceURL"], + "license": buildVars.addon_info["addon_license"], + "licenseURL": buildVars.addon_info["addon_licenseURL"], + } + with open(jsonFilename, "w") as addonStoreJson: + json.dump(addonStoreEntry, addonStoreJson, indent="\t") + + def generateManifest(source, dest): addon_info = buildVars.addon_info - addon_info["addon_version"] = env["addon_version"] - addon_info["addon_updateChannel"] = env["addon_updateChannel"] with codecs.open(source, "r", "utf-8") as f: manifest_template = f.read() - addon_info["addon_description"] = ''.join(addon_info["addon_description"]) manifest = manifest_template.format(**addon_info) with codecs.open(dest, "w", "utf-8") as f: f.write(manifest) + def generateTranslatedManifest(source, language, out): - # No ugettext in Python 3. - import sys - if sys.version_info.major == 2: - _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).ugettext - else: - _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext + _ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext vars = {} for var in ("addon_summary", "addon_description"): if isinstance(buildVars.addon_info[var], str): @@ -149,19 +275,24 @@ def generateTranslatedManifest(source, language, out): with codecs.open(out, "w", "utf-8") as f: f.write(result) + def expandGlobs(files): return [f for pattern in files for f in env.Glob(pattern)] + addon = env.NVDAAddon(addonFile, env.Dir('addon')) langDirs = [f for f in env.Glob(os.path.join("addon", "locale", "*"))] -#Allow all NVDA's gettext po files to be compiled in source/locale, and manifest files to be generated +# Allow all NVDA's gettext po files to be compiled in source/locale, and manifest files to be generated for dir in langDirs: poFile = dir.File(os.path.join("LC_MESSAGES", "nvda.po")) - moFile=env.gettextMoFile(poFile) + moFile = env.gettextMoFile(poFile) env.Depends(moFile, poFile) - translatedManifest = env.NVDATranslatedManifest(dir.File("manifest.ini"), [moFile, os.path.join("manifest-translated.ini.tpl")]) + translatedManifest = env.NVDATranslatedManifest( + dir.File("manifest.ini"), + [moFile, os.path.join("manifest-translated.ini.tpl")] + ) env.Depends(translatedManifest, ["buildVars.py"]) env.Depends(addon, [translatedManifest, moFile]) @@ -169,20 +300,26 @@ pythonFiles = expandGlobs(buildVars.pythonSources) for file in pythonFiles: env.Depends(addon, file) -#Convert markdown files to html -createAddonHelp("addon") # We need at least doc in English and should enable the Help button for the add-on in Add-ons Manager +# Convert markdown files to html +# We need at least doc in English and should enable the Help button for the add-on in Add-ons Manager +createAddonHelp("addon") for mdFile in env.Glob(os.path.join('addon', 'doc', '*', '*.md')): htmlFile = env.markdown(mdFile) - env.Depends(htmlFile, mdFile) + try: # It is possible that no moFile was set, because an add-on has no translations. + moFile + except NameError: # Runs if there is no moFile + env.Depends(htmlFile, mdFile) + else: # Runs if there is a moFile + env.Depends(htmlFile, [mdFile, moFile]) env.Depends(addon, htmlFile) # Pot target i18nFiles = expandGlobs(buildVars.i18nSources) -gettextvars={ - 'gettext_package_bugs_address' : 'nvda-translations@groups.io', - 'gettext_package_name' : buildVars.addon_info['addon_name'], - 'gettext_package_version' : buildVars.addon_info['addon_version'] - } +gettextvars = { + 'gettext_package_bugs_address': 'nvda-translations@groups.io', + 'gettext_package_name': buildVars.addon_info['addon_name'], + 'gettext_package_version': buildVars.addon_info['addon_version'] +} pot = env.gettextPotFile("${addon_name}.pot", i18nFiles, **gettextvars) env.Alias('pot', pot) @@ -198,3 +335,4 @@ env.Depends(manifest, "buildVars.py") env.Depends(addon, manifest) env.Default(addon) +env.Clean(addon, ['.sconsign.dblite', 'addon/doc/' + buildVars.baseLanguage + '/']) diff --git a/style.css b/style.css index 373283fc..90f99d14 100644 --- a/style.css +++ b/style.css @@ -1,8 +1,6 @@ @charset "utf-8"; body { font-family : Verdana, Arial, Helvetica, Sans-serif; -color : #FFFFFF; -background-color : #000000; line-height: 1.2em; } h1, h2 {text-align: center} @@ -26,5 +24,3 @@ a { text-decoration : underline; text-decoration : none; } a:focus, a:hover {outline: solid} -:link {color: #0000FF; -background-color: #FFFFFF}