MultiLang
/
sconstruct
327 строк · 11.8 Кб
1# NVDA add-on template SCONSTRUCT file
2# Copyright (C) 2012-2023 Rui Batista, Noelia Martinez, Joseph Lee
3# This file is covered by the GNU General Public License.
4# See the file COPYING.txt for more details.
5
6import codecs
7import gettext
8import os
9import os.path
10import zipfile
11import sys
12
13# While names imported below are available by default in every SConscript
14# Linters aren't aware about them.
15# To avoid Flake8 F821 warnings about them they are imported explicitly.
16# When using other Scons functions please add them to the line below.
17from SCons.Script import BoolVariable, Builder, Copy, Environment, Variables
18
19sys.dont_write_bytecode = True
20
21# Bytecode should not be written for build vars module to keep the repository root folder clean.
22import buildVars # NOQA: E402
23
24
25def md2html(source, dest):
26import markdown
27# Use extensions if defined.
28mdExtensions = buildVars.markdownExtensions
29lang = os.path.basename(os.path.dirname(source)).replace('_', '-')
30localeLang = os.path.basename(os.path.dirname(source))
31try:
32_ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[localeLang]).gettext
33summary = _(buildVars.addon_info["addon_summary"])
34except Exception:
35summary = buildVars.addon_info["addon_summary"]
36title = "{addonSummary} {addonVersion}".format(
37addonSummary=summary, addonVersion=buildVars.addon_info["addon_version"]
38)
39headerDic = {
40"[[!meta title=\"": "# ",
41"\"]]": " #",
42}
43with codecs.open(source, "r", "utf-8") as f:
44mdText = f.read()
45for k, v in headerDic.items():
46mdText = mdText.replace(k, v, 1)
47htmlText = markdown.markdown(mdText, extensions=mdExtensions)
48# Optimization: build resulting HTML text in one go instead of writing parts separately.
49docText = "\n".join([
50"<!DOCTYPE html>",
51"<html lang=\"%s\">" % lang,
52"<head>",
53"<meta charset=\"UTF-8\">"
54"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">",
55"<link rel=\"stylesheet\" type=\"text/css\" href=\"../style.css\" media=\"screen\">",
56"<title>%s</title>" % title,
57"</head>\n<body>",
58htmlText,
59"</body>\n</html>"
60])
61with codecs.open(dest, "w", "utf-8") as f:
62f.write(docText)
63
64
65def mdTool(env):
66mdAction = env.Action(
67lambda target, source, env: md2html(source[0].path, target[0].path),
68lambda target, source, env: 'Generating % s' % target[0],
69)
70mdBuilder = env.Builder(
71action=mdAction,
72suffix='.html',
73src_suffix='.md',
74)
75env['BUILDERS']['markdown'] = mdBuilder
76
77
78def validateVersionNumber(key, val, env):
79# Used to make sure version major.minor.patch are integers to comply with NV Access add-on store.
80# Ignore all this if version number is not specified, in which case json generator will validate this info.
81if val == "0.0.0":
82return
83versionNumber = val.split(".")
84if len(versionNumber) < 3:
85raise ValueError("versionNumber must have three parts (major.minor.patch)")
86if not all([part.isnumeric() for part in versionNumber]):
87raise ValueError("versionNumber (major.minor.patch) must be integers")
88
89
90vars = Variables()
91vars.Add("version", "The version of this build", buildVars.addon_info["addon_version"])
92vars.Add("versionNumber", "Version number of the form major.minor.patch", "0.0.0", validateVersionNumber)
93vars.Add(BoolVariable("dev", "Whether this is a daily development version", False))
94vars.Add("channel", "Update channel for this build", buildVars.addon_info["addon_updateChannel"])
95
96env = Environment(variables=vars, ENV=os.environ, tools=['gettexttool', mdTool])
97env.Append(**buildVars.addon_info)
98
99if env["dev"]:
100import datetime
101buildDate = datetime.datetime.now()
102year, month, day = str(buildDate.year), str(buildDate.month), str(buildDate.day)
103versionTimestamp = "".join([year, month.zfill(2), day.zfill(2)])
104env["addon_version"] = f"{versionTimestamp}.0.0"
105env["versionNumber"] = f"{versionTimestamp}.0.0"
106env["channel"] = "dev"
107elif env["version"] is not None:
108env["addon_version"] = env["version"]
109if "channel" in env and env["channel"] is not None:
110env["addon_updateChannel"] = env["channel"]
111
112buildVars.addon_info["addon_version"] = env["addon_version"]
113buildVars.addon_info["addon_updateChannel"] = env["addon_updateChannel"]
114
115addonFile = env.File("${addon_name}-${addon_version}.nvda-addon")
116
117
118def addonGenerator(target, source, env, for_signature):
119action = env.Action(
120lambda target, source, env: createAddonBundleFromPath(source[0].abspath, target[0].abspath) and None,
121lambda target, source, env: "Generating Addon %s" % target[0]
122)
123return action
124
125
126def manifestGenerator(target, source, env, for_signature):
127action = env.Action(
128lambda target, source, env: generateManifest(source[0].abspath, target[0].abspath) and None,
129lambda target, source, env: "Generating manifest %s" % target[0]
130)
131return action
132
133
134def translatedManifestGenerator(target, source, env, for_signature):
135dir = os.path.abspath(os.path.join(os.path.dirname(str(source[0])), ".."))
136lang = os.path.basename(dir)
137action = env.Action(
138lambda target, source, env: generateTranslatedManifest(source[1].abspath, lang, target[0].abspath) and None,
139lambda target, source, env: "Generating translated manifest %s" % target[0]
140)
141return action
142
143
144env['BUILDERS']['NVDAAddon'] = Builder(generator=addonGenerator)
145env['BUILDERS']['NVDAManifest'] = Builder(generator=manifestGenerator)
146env['BUILDERS']['NVDATranslatedManifest'] = Builder(generator=translatedManifestGenerator)
147
148
149def createAddonHelp(dir):
150docsDir = os.path.join(dir, "doc")
151if os.path.isfile("style.css"):
152cssPath = os.path.join(docsDir, "style.css")
153cssTarget = env.Command(cssPath, "style.css", Copy("$TARGET", "$SOURCE"))
154env.Depends(addon, cssTarget)
155if os.path.isfile("readme.md"):
156readmePath = os.path.join(docsDir, buildVars.baseLanguage, "readme.md")
157readmeTarget = env.Command(readmePath, "readme.md", Copy("$TARGET", "$SOURCE"))
158env.Depends(addon, readmeTarget)
159
160
161def createAddonBundleFromPath(path, dest):
162""" Creates a bundle from a directory that contains an addon manifest file."""
163basedir = os.path.abspath(path)
164with zipfile.ZipFile(dest, 'w', zipfile.ZIP_DEFLATED) as z:
165# FIXME: the include/exclude feature may or may not be useful. Also python files can be pre-compiled.
166for dir, dirnames, filenames in os.walk(basedir):
167relativePath = os.path.relpath(dir, basedir)
168for filename in filenames:
169pathInBundle = os.path.join(relativePath, filename)
170absPath = os.path.join(dir, filename)
171if pathInBundle not in buildVars.excludedFiles:
172z.write(absPath, pathInBundle)
173createAddonStoreJson(dest)
174return dest
175
176
177def createAddonStoreJson(bundle):
178"""Creates add-on store JSON file from an add-on package and manifest data."""
179import json
180import hashlib
181# Set different json file names and version number properties based on version number parsing results.
182if env["versionNumber"] == "0.0.0":
183env["versionNumber"] = buildVars.addon_info["addon_version"]
184versionNumberParsed = env["versionNumber"].split(".")
185if all([part.isnumeric() for part in versionNumberParsed]):
186if len(versionNumberParsed) == 1:
187versionNumberParsed += ["0", "0"]
188elif len(versionNumberParsed) == 2:
189versionNumberParsed.append("0")
190else:
191versionNumberParsed = []
192if len(versionNumberParsed):
193major, minor, patch = [int(part) for part in versionNumberParsed]
194jsonFilename = f'{major}.{minor}.{patch}.json'
195else:
196jsonFilename = f'{buildVars.addon_info["addon_version"]}.json'
197major, minor, patch = 0, 0, 0
198print('Generating % s' % jsonFilename)
199sha256 = hashlib.sha256()
200with open(bundle, "rb") as f:
201for byte_block in iter(lambda: f.read(65536), b""):
202sha256.update(byte_block)
203hashValue = sha256.hexdigest()
204try:
205minimumNVDAVersion = buildVars.addon_info["addon_minimumNVDAVersion"].split(".")
206except AttributeError:
207minimumNVDAVersion = [0, 0, 0]
208minMajor, minMinor = minimumNVDAVersion[:2]
209minPatch = minimumNVDAVersion[-1] if len(minimumNVDAVersion) == 3 else "0"
210try:
211lastTestedNVDAVersion = buildVars.addon_info["addon_lastTestedNVDAVersion"].split(".")
212except AttributeError:
213lastTestedNVDAVersion = [0, 0, 0]
214lastTestedMajor, lastTestedMinor = lastTestedNVDAVersion[:2]
215lastTestedPatch = lastTestedNVDAVersion[-1] if len(lastTestedNVDAVersion) == 3 else "0"
216channel = buildVars.addon_info["addon_updateChannel"]
217if channel is None:
218channel = "stable"
219addonStoreEntry = {
220"addonId": buildVars.addon_info["addon_name"],
221"displayName": buildVars.addon_info["addon_summary"],
222"URL": "",
223"description": buildVars.addon_info["addon_description"],
224"sha256": hashValue,
225"homepage": buildVars.addon_info["addon_url"],
226"addonVersionName": buildVars.addon_info["addon_version"],
227"addonVersionNumber": {
228"major": major,
229"minor": minor,
230"patch": patch
231},
232"minNVDAVersion": {
233"major": int(minMajor),
234"minor": int(minMinor),
235"patch": int(minPatch)
236},
237"lastTestedVersion": {
238"major": int(lastTestedMajor),
239"minor": int(lastTestedMinor),
240"patch": int(lastTestedPatch)
241},
242"channel": channel,
243"publisher": "",
244"sourceURL": buildVars.addon_info["addon_sourceURL"],
245"license": buildVars.addon_info["addon_license"],
246"licenseURL": buildVars.addon_info["addon_licenseURL"],
247}
248with open(jsonFilename, "w") as addonStoreJson:
249json.dump(addonStoreEntry, addonStoreJson, indent="\t")
250
251
252def generateManifest(source, dest):
253addon_info = buildVars.addon_info
254with codecs.open(source, "r", "utf-8") as f:
255manifest_template = f.read()
256manifest = manifest_template.format(**addon_info)
257with codecs.open(dest, "w", "utf-8") as f:
258f.write(manifest)
259
260
261def generateTranslatedManifest(source, language, out):
262_ = gettext.translation("nvda", localedir=os.path.join("addon", "locale"), languages=[language]).gettext
263vars = {}
264for var in ("addon_summary", "addon_description"):
265vars[var] = _(buildVars.addon_info[var])
266with codecs.open(source, "r", "utf-8") as f:
267manifest_template = f.read()
268result = manifest_template.format(**vars)
269with codecs.open(out, "w", "utf-8") as f:
270f.write(result)
271
272
273def expandGlobs(files):
274return [f for pattern in files for f in env.Glob(pattern)]
275
276
277addon = env.NVDAAddon(addonFile, env.Dir('addon'))
278
279langDirs = [f for f in env.Glob(os.path.join("addon", "locale", "*"))]
280
281# Allow all NVDA's gettext po files to be compiled in source/locale, and manifest files to be generated
282for dir in langDirs:
283poFile = dir.File(os.path.join("LC_MESSAGES", "nvda.po"))
284moFile = env.gettextMoFile(poFile)
285env.Depends(moFile, poFile)
286translatedManifest = env.NVDATranslatedManifest(
287dir.File("manifest.ini"),
288[moFile, os.path.join("manifest-translated.ini.tpl")]
289)
290env.Depends(translatedManifest, ["buildVars.py"])
291env.Depends(addon, [translatedManifest, moFile])
292
293pythonFiles = expandGlobs(buildVars.pythonSources)
294for file in pythonFiles:
295env.Depends(addon, file)
296
297# Convert markdown files to html
298# We need at least doc in English and should enable the Help button for the add-on in Add-ons Manager
299createAddonHelp("addon")
300for mdFile in env.Glob(os.path.join('addon', 'doc', '*', '*.md')):
301htmlFile = env.markdown(mdFile)
302env.Depends(htmlFile, mdFile)
303env.Depends(addon, htmlFile)
304
305# Pot target
306i18nFiles = expandGlobs(buildVars.i18nSources)
307gettextvars = {
308'gettext_package_bugs_address': 'nvda-translations@groups.io',
309'gettext_package_name': buildVars.addon_info['addon_name'],
310'gettext_package_version': buildVars.addon_info['addon_version']
311}
312
313pot = env.gettextPotFile("${addon_name}.pot", i18nFiles, **gettextvars)
314env.Alias('pot', pot)
315env.Depends(pot, i18nFiles)
316mergePot = env.gettextMergePotFile("${addon_name}-merge.pot", i18nFiles, **gettextvars)
317env.Alias('mergePot', mergePot)
318env.Depends(mergePot, i18nFiles)
319
320# Generate Manifest path
321manifest = env.NVDAManifest(os.path.join("addon", "manifest.ini"), os.path.join("manifest.ini.tpl"))
322# Ensure manifest is rebuilt if buildVars is updated.
323env.Depends(manifest, "buildVars.py")
324
325env.Depends(addon, manifest)
326env.Default(addon)
327env.Clean(addon, ['.sconsign.dblite', 'addon/doc/' + buildVars.baseLanguage + '/'])
328