mirror of
https://github.com/onyx-and-iris/nvda-addon-voicemeeter.git
synced 2024-11-15 15:00:47 +00:00
333 lines
12 KiB
Plaintext
333 lines
12 KiB
Plaintext
|
# 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 + '/'])
|