nvda-addon-voicemeeter/sconstruct

333 lines
12 KiB
Plaintext
Raw Permalink Normal View History

2023-09-23 22:55:58 +01:00
# NVDA add-on template SCONSTRUCT file
# 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
def md2html(source, dest):
import markdown
# Use extensions if defined.
mdExtensions = buildVars.markdownExtensions
lang = os.path.basename(os.path.dirname(source)).replace('_', '-')
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=\"": "# ",
"\"]]": " #",
}
with codecs.open(source, "r", "utf-8") as f:
mdText = f.read()
for k, v in headerDic.items():
mdText = mdText.replace(k, v, 1)
htmlText = markdown.markdown(mdText, extensions=mdExtensions)
# Optimization: build resulting HTML text in one go instead of writing parts separately.
docText = "\n".join([
"<!DOCTYPE html>",
"<html lang=\"%s\">" % lang,
"<head>",
"<meta charset=\"UTF-8\">"
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">",
"<link rel=\"stylesheet\" type=\"text/css\" href=\"../style.css\" media=\"screen\">",
"<title>%s</title>" % title,
"</head>\n<body>",
htmlText,
"</body>\n</html>"
])
with codecs.open(dest, "w", "utf-8") as f:
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],
)
mdBuilder = env.Builder(
action=mdAction,
suffix='.html',
src_suffix='.md',
)
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"])
env = Environment(variables=vars, ENV=os.environ, tools=['gettexttool', mdTool])
env.Append(**buildVars.addon_info)
if env["dev"]:
import datetime
buildDate = datetime.datetime.now()
year, month, day = str(buildDate.year), str(buildDate.month), str(buildDate.day)
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"]
buildVars.addon_info["addon_version"] = env["addon_version"]
buildVars.addon_info["addon_updateChannel"] = env["addon_updateChannel"]
addonFile = env.File("${addon_name}-${addon_version}.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]
)
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]
)
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]
)
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"):
cssPath = os.path.join(docsDir, "style.css")
cssTarget = env.Command(cssPath, "style.css", Copy("$TARGET", "$SOURCE"))
env.Depends(addon, cssTarget)
if os.path.isfile("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)
with zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) as z:
# FIXME: the include/exclude feature may or may not be useful. Also python files can be pre-compiled.
for dir, dirnames, filenames in os.walk(basedir):
relativePath = os.path.relpath(dir, basedir)
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)
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'
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
with codecs.open(source, "r", "utf-8") as f:
manifest_template = f.read()
manifest = manifest_template.format(**addon_info)
with codecs.open(dest, "w", "utf-8") as f:
f.write(manifest)
def generateTranslatedManifest(source, language, out):
_ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext
vars = {}
for var in ("addon_summary", "addon_description"):
vars[var] = _(buildVars.addon_info[var])
with codecs.open(source, "r", "utf-8") as f:
manifest_template = f.read()
result = manifest_template.format(**vars)
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
for dir in langDirs:
poFile = dir.File(os.path.join("LC_MESSAGES", "nvda.po"))
moFile = env.gettextMoFile(poFile)
env.Depends(moFile, poFile)
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])
pythonFiles = expandGlobs(buildVars.pythonSources)
for file in pythonFiles:
env.Depends(addon, file)
# 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)
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']
}
pot = env.gettextPotFile("${addon_name}.pot", i18nFiles, **gettextvars)
env.Alias('pot', pot)
env.Depends(pot, i18nFiles)
mergePot = env.gettextMergePotFile("${addon_name}-merge.pot", i18nFiles, **gettextvars)
env.Alias('mergePot', mergePot)
env.Depends(mergePot, i18nFiles)
# Generate Manifest path
manifest = env.NVDAManifest(os.path.join("addon", "manifest.ini"), os.path.join("manifest.ini.tpl"))
# Ensure manifest is rebuilt if buildVars is updated.
env.Depends(manifest, "buildVars.py")
env.Depends(addon, manifest)
env.Default(addon)
env.Clean(addon, ['.sconsign.dblite', 'addon/doc/' + buildVars.baseLanguage + '/'])