#!/usr/bin/env python3 # python3-sphinxcontrib-applehelp probe -- the half of 99-python3-sphinxcontrib- # applehelp.test that needs an interpreter. # # WHY A PROBE AT ALL, AND WHY IT STUBS SPHINX. # # sphinxcontrib/applehelp/__init__.py opens with nine unconditional `sphinx' # imports at MODULE scope (lines 11-19) plus a tenth chosen on version. This # image installs no Sphinx, so `import sphinxcontrib.applehelp' raises # ModuleNotFoundError and every statement in the file is unreachable. That fact # is measured first, by `bare_import', BEFORE anything is stubbed -- it is the # image's real answer and the suite asserts it as such. # # What is left to assert is the package's CONTRACT WITH SPHINX, which is the # whole of what a Sphinx extension is: the builder class it defines, the dict # setup() returns, the config values it registers with their defaults and types, # and what its build steps do with a configuration. None of that needs Sphinx # installed -- it needs the names Sphinx would have provided. So the probe # injects a stub `sphinx' package into sys.modules and then imports the # package's OWN code, which runs unmodified. Every value below is produced by # sphinxcontrib.applehelp, not by the stub. # # WHERE THE STUB MUST BE FAITHFUL, IT IS FAITHFUL, AND THE PLACES ARE NAMED: # # make_filename -- applehelp's default `applehelp_bundle_name' and # `applehelp_kb_product' are lambdas that CALL it, so a # wrong stub would silently change the value asserted. # Sphinx's is re.sub('[^a-zA-Z0-9_-]', '', s) or 'sphinx'. # progress_message -- Sphinx's is a decorator AND a context manager whose # __exit__ SWALLOWS SkipProgressMessage. That swallowing # is the documented refusal path this port turns on, so # the stub reproduces it exactly rather than letting the # exception escape. # SphinxError -- a real Exception subclass, since applehelp derives two # error classes from it and the suite asserts the chain. # ensuredir -- os.makedirs(..., exist_ok=True), the real behaviour; # build_helpbook depends on the directory existing. # # Everything else the stub provides is a RECORDER: it captures the call the # package made and performs no work. That is deliberate -- what is being measured # is the package's REQUEST of Sphinx, not Sphinx's execution of it, and a # recorder cannot accidentally pass a case by doing the work itself. # # THE INDEXING BOUNDARY IS macOS-ONLY AND IS ASSERTED AS A BOUNDARY. # build_helpindex() and do_codesign() shell out to /usr/bin/hiutil and # /usr/bin/codesign, which exist on no Linux image. The port is asserted up to # that line and the line itself is asserted to be a documented refusal rather # than a crash, in both of its two shapes: with applehelp_disable_external_tools # set, a SkipProgressMessage carrying the exact command a user must run; without # it, a typed AppleHelpIndexerFailed / AppleHelpCodeSigningFailed naming the # missing tool. Neither is a traceback out of subprocess. # # Output protocol: one `key=value' line per measurement on stdout. Values that # can carry spaces or newlines go through esc(); the suite compares the escaped # form. A measurement that raises is emitted as `EXC:' plus a `_detail' # line, so one surprise costs one case instead of every case after it. import atexit import hashlib import os import plistlib import re import shlex import sys import tempfile import types OUT = [] # Written from atexit rather than at the end of the file: a probe that dies # half-way still hands the suite everything it had already measured, so one # surprise costs the cases after it and not the cases before it too. atexit.register(lambda: sys.stdout.write("\n".join(OUT) + "\n")) def emit(k, v): OUT.append("%s=%s" % (k, v)) def esc(s): return (str(s).replace("\\", "\\\\").replace("\n", "\\n") .replace("\t", "\\t").replace(" ", "\\s")) def guard(key, fn): """Run fn and emit its value, or emit the exception type.""" try: emit(key, fn()) except Exception as exc: # noqa: BLE001 -- the type IS the measurement emit(key, "EXC:%s" % type(exc).__name__) emit(key + "_detail", esc(str(exc))[:200]) # --------------------------------------------------------------------------- # 1. The image's own answer, measured before anything is stubbed. # --------------------------------------------------------------------------- def _bare_import(): try: import sphinxcontrib.applehelp # noqa: F401 return "imported" except ModuleNotFoundError as exc: return "ModuleNotFoundError:%s" % exc.name except Exception as exc: # noqa: BLE001 return "EXC:%s" % type(exc).__name__ emit("bare_import", _bare_import()) # The failed import leaves a partially-initialised entry behind; drop every # sphinxcontrib.* entry so the stubbed import below starts clean. for _n in [n for n in list(sys.modules) if n.startswith("sphinxcontrib.")]: del sys.modules[_n] # --------------------------------------------------------------------------- # 2. The stub `sphinx' package. # --------------------------------------------------------------------------- CALLS = {"copy_asset": [], "copy_asset_file": [], "matcher": [], "warnings": [], "progress": [], "skips": []} class SphinxError(Exception): category = "Sphinx error" class SkipProgressMessage(Exception): pass class StandaloneHTMLBuilder(object): """The base applehelp subclasses. Only the attributes applehelp's own code reads or overrides are present; init() and handle_finish() are the two super() calls it makes.""" name = "html" format = "html" epilog = "The HTML pages are in %(outdir)s." copysource = True add_permalinks = True embedded = False search = True supported_image_types = [] out_suffix = ".html" link_suffix = ".html" def __init__(self): self.super_init_called = False self.super_finish_called = False def init(self): self.super_init_called = True def handle_finish(self): self.super_finish_called = True class _Logger(object): def warning(self, msg, *args): CALLS["warnings"].append(msg % args if args else msg) def info(self, msg, *args): pass class _progress_message(object): """Sphinx's progress_message, reduced to the behaviour applehelp depends on. The swallow of SkipProgressMessage in __exit__ is the whole point: it is how upstream turns `applehelp_disable_external_tools' into a printed instruction instead of an error, and a stub that let the exception escape would report that documented path as a crash.""" def __init__(self, message): self.message = message def __enter__(self): CALLS["progress"].append(self.message) return self def __exit__(self, typ, val, tb): if isinstance(val, SkipProgressMessage): CALLS["skips"].append((self.message, val.args)) return True return False def __call__(self, f): def wrapper(*args, **kwargs): with self: return f(*args, **kwargs) wrapper.__name__ = f.__name__ wrapper.__doc__ = f.__doc__ wrapper.__wrapped__ = f return wrapper def _make_filename(string): # Sphinx's sphinx.util.osutil.make_filename, verbatim in behaviour. return re.sub('[^a-zA-Z0-9_-]', '', string) or 'sphinx' def _ensuredir(path): os.makedirs(path, exist_ok=True) def _copy_asset(source, destination, excluded=None, context=None, renderer=None, **kw): CALLS["copy_asset"].append((source, destination)) def _copy_asset_file(source, destination, context=None, renderer=None, **kw): CALLS["copy_asset_file"].append((source, destination, context)) class _Matcher(object): def __init__(self, patterns): CALLS["matcher"].append(list(patterns)) self.patterns = list(patterns) def __call__(self, string): return False def _get_translation(catalog, namespace="general"): def gettext(message, *args): return message return gettext def _mod(name, **attrs): m = types.ModuleType(name) for k, v in attrs.items(): setattr(m, k, v) sys.modules[name] = m return m # The version the stub claims is poky's own python3-sphinx (7.2.6) -- the Sphinx # this image would install if it installed one. It selects the >= 6.1 import # branch at applehelp/__init__.py:21, which is the branch a landed Sphinx would # select, so the module under test is the module that would really run. _sphinx = _mod("sphinx", version_info=(7, 2, 6, "final", 0, ""), __version__="7.2.6") _mod("sphinx.application", Sphinx=object) _builders = _mod("sphinx.builders") _html = _mod("sphinx.builders.html", StandaloneHTMLBuilder=StandaloneHTMLBuilder) _builders.html = _html _sphinx.builders = _builders _sphinx.application = sys.modules["sphinx.application"] _mod("sphinx.errors", SphinxError=SphinxError) _sphinx.errors = sys.modules["sphinx.errors"] _mod("sphinx.locale", get_translation=_get_translation) _sphinx.locale = sys.modules["sphinx.locale"] _logging = _mod("sphinx.util.logging", getLogger=lambda name: _Logger()) _util = _mod("sphinx.util", logging=_logging) _sphinx.util = _util _mod("sphinx.util.fileutil", copy_asset=_copy_asset, copy_asset_file=_copy_asset_file) _mod("sphinx.util.matching", Matcher=_Matcher) _mod("sphinx.util.osutil", ensuredir=_ensuredir, make_filename=_make_filename) _mod("sphinx.util.display", SkipProgressMessage=SkipProgressMessage, progress_message=_progress_message) for _sub in ("logging", "fileutil", "matching", "osutil", "display"): setattr(_util, _sub, sys.modules["sphinx.util." + _sub]) # The environment the two codesign defaults read. Set to known values so the # lambdas are measurable rather than dependent on the guest's environment. os.environ["CODE_SIGN_IDENTITY"] = "Fixture Signing Identity" os.environ["OTHER_CODE_SIGN_FLAGS"] = "--timestamp --deep" import sphinxcontrib # noqa: E402 import sphinxcontrib.applehelp as A # noqa: E402 # --------------------------------------------------------------------------- # 3. Identity and packaging. # --------------------------------------------------------------------------- emit("version", A.__version__) emit("version_info", ".".join(str(n) for n in A.__version_info__)) emit("module_file", A.__file__) emit("package_dir", A.package_dir) emit("template_dir", A.template_dir) try: from importlib.metadata import distribution as _dist except ImportError: # pragma: no cover -- 3.12 always has it _dist = None guard("dist_version", lambda: _dist("sphinxcontrib-applehelp").version) guard("dist_name", lambda: _dist("sphinxcontrib-applehelp").metadata["Name"]) # Upstream states Sphinx as an OPTIONAL extra, never as a plain requirement -- # `Sphinx>=5 ; extra == "standalone"'. The module imports it unconditionally # anyway. Both halves of that disagreement are asserted. guard("dist_requires", lambda: esc(",".join(_dist("sphinxcontrib-applehelp").requires or []))) guard("dist_requires_plain", lambda: esc(",".join( r for r in (_dist("sphinxcontrib-applehelp").requires or []) if ";" not in r))) guard("ep_count", lambda: len(list(_dist("sphinxcontrib-applehelp").entry_points))) # `sphinxcontrib' is a PEP 420 implicit namespace package -- no __init__.py -- # which is how this port and its sibling sphinxcontrib-* ports share one # top-level name without any of them owning it. emit("ns_file", getattr(sphinxcontrib, "__file__", None)) emit("ns_path_type", type(sphinxcontrib.__path__).__name__) emit("ns_path", ",".join(sphinxcontrib.__path__)) emit("api_surface", ",".join(sorted(n for n in dir(A) if not n.startswith("_")))) emit("api_callables", ",".join(sorted( n for n in dir(A) if not n.startswith("_") and callable(getattr(A, n))))) # --------------------------------------------------------------------------- # 4. The builder class, as applehelp itself declares it. # --------------------------------------------------------------------------- B = A.AppleHelpBuilder emit("builder_name", B.name) emit("builder_base", B.__bases__[0].__name__) # `format' is NOT applehelp's to declare -- it is inherited from # StandaloneHTMLBuilder. Asserting its VALUE here would assert the stub, so what # is asserted is that the class does not carry one of its own. emit("builder_own_format", "absent" if "format" not in B.__dict__ else "own") emit("builder_own_attrs", ",".join(sorted( k for k in B.__dict__ if not k.startswith("__")))) emit("builder_copysource", B.copysource) emit("builder_add_permalinks", B.add_permalinks) emit("builder_embedded", B.embedded) emit("builder_search", B.search) emit("builder_images", ",".join(B.supported_image_types)) emit("builder_epilog_head", esc(B.epilog.split("\n")[0])) emit("err_indexer_base", A.AppleHelpIndexerFailed.__bases__[0].__name__) emit("err_indexer_category", esc(A.AppleHelpIndexerFailed.category)) emit("err_codesign_base", A.AppleHelpCodeSigningFailed.__bases__[0].__name__) emit("err_codesign_category", esc(A.AppleHelpCodeSigningFailed.category)) # --------------------------------------------------------------------------- # 5. setup(): what the extension asks of Sphinx. # --------------------------------------------------------------------------- class _App(object): """The smallest object setup() actually uses. Every call is recorded rather than executed, so what is measured is the package's REQUEST of Sphinx.""" def __init__(self): self.required = [] self.extensions = [] self.builders = [] self.catalogs = [] self.configs = [] # (name, default, rebuild, types) def require_sphinx(self, version): self.required.append(version) def setup_extension(self, name): self.extensions.append(name) def add_builder(self, builder): self.builders.append(builder) def add_message_catalog(self, name, locale_dir): self.catalogs.append((name, locale_dir)) def add_config_value(self, name, default, rebuild, types=()): self.configs.append((name, default, rebuild, types)) app = _App() meta = A.setup(app) emit("setup_type", type(meta).__name__) emit("setup_keys", ",".join(sorted(meta))) emit("setup_version", meta.get("version")) emit("setup_parallel_read", meta.get("parallel_read_safe")) emit("setup_parallel_write", meta.get("parallel_write_safe")) emit("require_sphinx", ",".join(app.required)) emit("setup_extension", ",".join(app.extensions)) emit("builders_added", ",".join(b.__name__ for b in app.builders)) emit("catalog_name", ",".join(n for n, _ in app.catalogs)) emit("catalog_dir", ",".join(d for _, d in app.catalogs)) # --------------------------------------------------------------------------- # 6. The config values, with the lambda defaults resolved against a fixture. # --------------------------------------------------------------------------- emit("cfg_count", len(app.configs)) emit("cfg_names", ",".join(sorted(n for n, _, _, _ in app.configs))) # Every one of them rebuilds the `applehelp' target and nothing else. emit("cfg_rebuilds", ",".join(sorted(set(r for _, _, r, _ in app.configs)))) emit("cfg_typed", ",".join(sorted( "%s:%s" % (n, ",".join(t.__name__ for t in ts)) for n, _, _, ts in app.configs if ts))) emit("cfg_lambda_names", ",".join(sorted( n for n, d, _, _ in app.configs if callable(d)))) class _Config(object): """A fixture configuration. `project', `release' and `language' are the three attributes applehelp's default lambdas read.""" project = "Fixture Project" release = "2.5" language = None master_doc = "index" exclude_patterns = ["_build", "*.tmp"] _fixture = _Config() _defaults = {} for _n, _d, _r, _t in app.configs: _defaults[_n] = _d(_fixture) if callable(_d) else _d for _key in sorted(_defaults): emit("dflt_" + _key.replace("applehelp_", ""), esc(_defaults[_key])) # --------------------------------------------------------------------------- # 7. init(): the bundle path computation, over the fixture configuration. # --------------------------------------------------------------------------- class _Cfg(object): def __init__(self, **kw): for k, v in _defaults.items(): setattr(self, k, v) self.project = _fixture.project self.release = _fixture.release self.language = None self.master_doc = "index" self.exclude_patterns = list(_fixture.exclude_patterns) for k, v in kw.items(): setattr(self, k, v) WORK = tempfile.mkdtemp(prefix="applehelp-probe.") def _builder(outdir=None, **cfgkw): b = B() b.config = _Cfg(**cfgkw) b.outdir = outdir if outdir is not None else os.path.join(WORK, "out") b.confdir = os.path.join(WORK, "conf") b.srcdir = os.path.join(WORK, "src") b.globalcontext = {} b.templates = None return b # Without applehelp_bundle_id, init() must refuse -- that is the one thing a # project MUST set, and upstream raises rather than inventing one. def _init_no_id(): b = _builder() try: b.init() return "no-error" except SphinxError as exc: return "SphinxError:" + esc(str(exc)) guard("init_no_bundle_id", _init_no_id) _b = _builder(applehelp_bundle_id="com.example.fixture") _b.init() emit("init_super_called", _b.super_init_called) emit("init_out_suffix", _b.out_suffix) emit("init_link_suffix", _b.link_suffix) emit("init_bundle_path", _b.bundle_path) emit("init_bundle_rel", os.path.relpath(_b.bundle_path, WORK)) emit("init_outdir_rel", os.path.relpath(_b.outdir, WORK)) # A non-default bundle name and locale must both reach the path. _b2 = _builder(applehelp_bundle_id="com.example.other", applehelp_bundle_name="OtherBook", applehelp_locale="fr") _b2.init() emit("init_outdir_rel_fr", os.path.relpath(_b2.outdir, WORK)) _b3 = _builder(applehelp_bundle_id="com.example.fixture") _b3.handle_finish = B.handle_finish.__get__(_b3, B) class _Tasks(object): def __init__(self): self.added = [] def add_task(self, fn): self.added.append(getattr(fn, "__name__", str(fn))) _b3.finish_tasks = _Tasks() _b3.handle_finish() emit("finish_super_called", _b3.super_finish_called) emit("finish_tasks", ",".join(_b3.finish_tasks.added)) # --------------------------------------------------------------------------- # 8. Info.plist -- written for real, read back with plistlib. # --------------------------------------------------------------------------- _cdir = os.path.join(WORK, "Contents") os.makedirs(_cdir, exist_ok=True) _bp = _builder(applehelp_bundle_id="com.example.fixture") _bp.init() _bp.build_info_plist(_cdir) with open(os.path.join(_cdir, "Info.plist"), "rb") as _fh: _plist = plistlib.load(_fh) emit("plist_keys", ",".join(sorted(_plist))) emit("plist_identifier", _plist.get("CFBundleIdentifier")) emit("plist_devregion", _plist.get("CFBundleDevelopmentRegion")) emit("plist_dictversion", _plist.get("CFBundleInfoDictionaryVersion")) emit("plist_packagetype", _plist.get("CFBundlePackageType")) emit("plist_shortversion", _plist.get("CFBundleShortVersionString")) emit("plist_signature", _plist.get("CFBundleSignature")) emit("plist_version", _plist.get("CFBundleVersion")) emit("plist_accesspath", _plist.get("HPDBookAccessPath")) emit("plist_indexpath", _plist.get("HPDBookIndexPath")) emit("plist_title", esc(_plist.get("HPDBookTitle"))) emit("plist_booktype", _plist.get("HPDBookType")) emit("plist_extviewer", _plist.get("HPDBookUsesExternalViewer")) # The three optional key groups appear only when their config value is set. _cdir2 = os.path.join(WORK, "Contents2") os.makedirs(_cdir2, exist_ok=True) _bp2 = _builder(applehelp_bundle_id="com.example.fixture", applehelp_icon="icon.png", applehelp_kb_url="https://example.invalid/kb", applehelp_kb_product="fixture-kb", applehelp_remote_url="https://example.invalid/remote") _bp2.init() _bp2.build_info_plist(_cdir2) with open(os.path.join(_cdir2, "Info.plist"), "rb") as _fh: _plist2 = plistlib.load(_fh) emit("plist_optional_keys", ",".join(sorted(set(_plist2) - set(_plist)))) emit("plist_iconpath", _plist2.get("HPDBookIconPath")) emit("plist_kbproduct", _plist2.get("HPDBookKBProduct")) emit("plist_kburl", _plist2.get("HPDBookKBURL")) emit("plist_remoteurl", _plist2.get("HPDBookRemoteURL")) # --------------------------------------------------------------------------- # 9. The access page -- the call, not the rendering. # --------------------------------------------------------------------------- # copy_asset_file's rendering is Sphinx's (Jinja2), which this image does not # install; what belongs to applehelp is WHICH template it hands over, WHERE, and # with WHAT context, so those three are what is recorded. CALLS["copy_asset_file"] = [] _ba = _builder(applehelp_bundle_id="com.example.fixture") _ba.init() _lang = os.path.join(WORK, "lang") os.makedirs(_lang, exist_ok=True) _ba.build_access_page(_lang) if CALLS["copy_asset_file"]: _src, _dst, _ctx = CALLS["copy_asset_file"][-1] emit("access_src", _src) emit("access_src_name", os.path.basename(_src)) emit("access_src_exists", "yes" if os.path.isfile(_src) else "no") emit("access_dst_rel", os.path.relpath(_dst, WORK)) emit("access_ctx", esc(";".join("%s=%s" % (k, _ctx[k]) for k in sorted(_ctx)))) else: emit("access_src", "no-call") # copy_localized_files does nothing when the source directory is absent, and # builds a Matcher over exclude_patterns + '**/.*' when it is present. CALLS["copy_asset"] = [] CALLS["matcher"] = [] _bl = _builder(applehelp_bundle_id="com.example.fixture") _bl.init() _bl.copy_localized_files() emit("localized_absent_copies", len(CALLS["copy_asset"])) os.makedirs(os.path.join(_bl.confdir, "en.lproj"), exist_ok=True) _bl.copy_localized_files() emit("localized_present_copies", len(CALLS["copy_asset"])) emit("localized_matcher", esc(",".join(CALLS["matcher"][-1])) if CALLS["matcher"] else "no-matcher") # --------------------------------------------------------------------------- # 10. The macOS-only boundary, in both of its documented shapes. # --------------------------------------------------------------------------- def _skip_shape(method, **cfgkw): """With applehelp_disable_external_tools set, the step must raise SkipProgressMessage carrying the command line -- and progress_message must absorb it, so nothing propagates to the caller.""" CALLS["skips"] = [] b = _builder(outdir="/OUT", applehelp_disable_external_tools=True, **cfgkw) b.init() if method == "build_helpindex": b.build_helpindex("/LANG") else: b.do_codesign() if not CALLS["skips"]: return "no-skip" msg, args = CALLS["skips"][-1] return esc(" ".join(str(a) for a in args[1:]) if len(args) > 1 else msg) guard("helpindex_skip_absorbed", lambda: _skip_shape( "build_helpindex", applehelp_bundle_id="com.example.fixture")) guard("codesign_skip_absorbed", lambda: _skip_shape( "do_codesign", applehelp_bundle_id="com.example.fixture", applehelp_codesign_identity="Fixture Signing Identity")) # The message the skip carries is upstream's own instruction text. CALLS["skips"] = [] _bi = _builder(applehelp_bundle_id="com.example.fixture", applehelp_disable_external_tools=True) _bi.init() _bi.build_helpindex(os.path.join(WORK, "lang")) emit("helpindex_skip_msg_head", esc(CALLS["skips"][-1][0]) if CALLS["skips"] else "no-skip") def _indexer_args(**cfgkw): CALLS["skips"] = [] b = _builder(outdir="/OUT", applehelp_bundle_id="com.example.fixture", applehelp_disable_external_tools=True, **cfgkw) b.init() b.build_helpindex("/LANG") return esc(CALLS["skips"][-1][1][1]) if CALLS["skips"] else "no-skip" # UPSTREAM QUIRK, MEASURED AND ASSERTED AS IT IS. `applehelp_index_anchors' # defaults to False, but build_helpindex() gates on `is not None', so the # default configuration always appends `-a'. The flag is therefore unreachable # in its off position from configuration alone. The suite asserts the behaviour # that exists rather than the one the config value implies, so an upstream # repair shows up as a red rather than passing unnoticed. guard("helpindex_args_default", lambda: _indexer_args()) guard("helpindex_anchors_always", lambda: "yes" if " -a " in _indexer_args().replace("\\s", " ") else "no") guard("helpindex_args_anchors", lambda: _indexer_args( applehelp_index_anchors=True)) guard("helpindex_args_minterm", lambda: _indexer_args( applehelp_min_term_length=4)) guard("helpindex_args_stopwords", lambda: _indexer_args( applehelp_stopwords="fr")) def _codesign_args(**cfgkw): CALLS["skips"] = [] b = _builder(outdir="/OUT", applehelp_bundle_id="com.example.fixture", applehelp_disable_external_tools=True, applehelp_codesign_identity="Fixture Signing Identity", **cfgkw) b.init() b.do_codesign() return esc(CALLS["skips"][-1][1][1]) if CALLS["skips"] else "no-skip" guard("codesign_args_default", lambda: _codesign_args()) guard("codesign_args_flags", lambda: _codesign_args( applehelp_codesign_flags=["--force"])) # Without the disable flag the tool is actually invoked, and on this image it is # absent: upstream must turn the OSError into its own typed error rather than # letting subprocess's escape. def _missing_tool(method): b = _builder(applehelp_bundle_id="com.example.fixture", applehelp_codesign_identity="Fixture Signing Identity") b.init() try: if method == "build_helpindex": b.build_helpindex(os.path.join(WORK, "lang")) else: b.do_codesign() return "no-error" except Exception as exc: # noqa: BLE001 -- the type IS the measurement return "%s:%s" % (type(exc).__name__, esc(str(exc))) guard("helpindex_missing_tool", lambda: _missing_tool("build_helpindex")) guard("codesign_missing_tool", lambda: _missing_tool("do_codesign")) emit("indexer_path_present", "yes" if os.path.exists(_defaults["applehelp_indexer_path"]) else "no") emit("codesign_path_present", "yes" if os.path.exists(_defaults["applehelp_codesign_path"]) else "no") # --------------------------------------------------------------------------- # 11. The wheel's own RECORD, re-measured against the installed bytes. # --------------------------------------------------------------------------- import base64 # noqa: E402 def _record_check(): site = os.path.dirname(os.path.dirname(A.package_dir)) rec = os.path.join( site, "sphinxcontrib_applehelp-1.0.8.dist-info", "RECORD") rows = 0 bad = [] with open(rec) as fh: for line in fh: line = line.rstrip("\n") if not line: continue rows += 1 parts = line.rsplit(",", 2) if len(parts) != 3 or not parts[1]: continue # RECORD itself carries no digest name, digest, size = parts path = os.path.join(site, name) try: with open(path, "rb") as fh2: data = fh2.read() except OSError: bad.append(name + "(missing)") continue want = digest.split("=", 1)[1] got = base64.urlsafe_b64encode( hashlib.sha256(data).digest()).rstrip(b"=").decode() if got != want: bad.append(name + "(sha256)") elif int(size) != len(data): bad.append(name + "(size)") return rows, bad guard("record_rows", lambda: _record_check()[0]) guard("record_mismatched", lambda: ",".join(sorted(_record_check()[1]))) # --------------------------------------------------------------------------- # 12. The message catalogs the package ships and registers. # --------------------------------------------------------------------------- _loc = os.path.join(A.package_dir, "locales") emit("locales_dir_is_catalog", "yes" if _loc == app.catalogs[0][1] else "no") # `.tx' is the Transifex client's configuration directory, not a language. _langs = sorted(d for d in os.listdir(_loc) if not d.startswith(".") and os.path.isdir(os.path.join(_loc, d))) emit("locales_lang_count", len(_langs)) emit("locales_has_ru", "yes" if "ru" in _langs else "no") emit("locales_pot", "yes" if os.path.isfile( os.path.join(_loc, "sphinxcontrib.applehelp.pot")) else "no") def _mo_parse(): import gettext bad = [] for lang in _langs: mo = os.path.join(_loc, lang, "LC_MESSAGES", "sphinxcontrib.applehelp.mo") if not os.path.isfile(mo): bad.append(lang + "(absent)") continue try: with open(mo, "rb") as fh: gettext.GNUTranslations(fh) except Exception: # noqa: BLE001 bad.append(lang + "(unparseable)") return ",".join(bad) guard("locales_mo_bad", _mo_parse)