# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. import json import sys from pathlib import Path import jsonschema import yaml HEADER_LINE = ( "// This file was generated by generate_feature_manifest.py from FeatureManifest.yaml." " DO NOT EDIT.\n" ) FEATURE_SCHEMA = Path("schemas", "ExperimentFeature.schema.json") NIMBUS_FALLBACK_PREFS = ( "constexpr std::pair" "NIMBUS_FALLBACK_PREFS[]{{{}}};" ) # Do not add new feature IDs to this list! isEarlyStartup is being deprecated. # See https://bugzilla.mozilla.org/show_bug.cgi?id=1875331 for details. ALLOWED_ISEARLYSTARTUP_FEATURE_IDS = { "abouthomecache", "aboutwelcome", "dapTelemetry", "gleanInternalSdk", "majorRelease2022", "newtab", "pocketNewtab", "saveToPocket", "searchConfiguration", "shellService", "testFeature", "updatePrompt", "upgradeDialog", } def write_fm_headers(fd): fd.write(HEADER_LINE) def validate_feature_manifest(schema_path, manifest_path, manifest): with open(schema_path, "r") as f: schema = json.load(f) set_prefs = {} fallback_prefs = {} for feature_id, feature in manifest.items(): try: jsonschema.validate(feature, schema) is_early_startup = feature.get("isEarlyStartup", False) allowed_is_early_startup = feature_id in ALLOWED_ISEARLYSTARTUP_FEATURE_IDS if is_early_startup != allowed_is_early_startup: if is_early_startup: print(f"Feature {feature_id} is marked isEarlyStartup: true") print( "isEarlyStartup is deprecated and no new isEarlyStartup features can be added" ) print( "See https://bugzilla.mozilla.org/show_bug.cgi?id=1875331 for details" ) raise Exception("isEarlyStartup is deprecated") else: print( f"Feature {feature_id} is not early startup but is in the allow list." ) print("Please remove it from generate_feature_manifest.py") raise Exception("isEarlyStartup is deprecated") for variable, variable_def in feature.get("variables", {}).items(): set_pref = variable_def.get("setPref") if isinstance(set_pref, dict): set_pref = set_pref.get("pref") if set_pref is not None: if set_pref in set_prefs: other_feature = set_prefs[set_pref][0] other_variable = set_prefs[set_pref][1] print("Multiple variables cannot declare the same setPref") print( f"{feature_id} variable {variable} wants to set pref {set_pref}" ) print( f"{other_feature} variable {other_variable} wants to set pref " f"{set_pref}" ) raise Exception("Set prefs are exclusive") set_prefs[set_pref] = (feature_id, variable) fallback_pref = variable_def.get("fallbackPref") if fallback_pref is not None: fallback_prefs[fallback_pref] = (feature_id, variable) conflicts = [ ( "setPref", fallback_pref, "fallbackPref", set_prefs.get(fallback_pref), ), ("fallbackPref", set_pref, "setPref", fallback_prefs.get(set_pref)), ] for kind, pref, other_kind, conflict in conflicts: if conflict is not None: print( "The same pref cannot be specified in setPref and fallbackPref" ) print( f"{feature_id} variable {variable} has specified {kind} {pref}" ) print( f"{conflict[0]} variable {conflict[1]} has specified {other_kind} " f"{pref}" ) raise Exception("Set prefs and fallback prefs cannot overlap") except Exception as e: print("Error while validating FeatureManifest.yaml") print(f"On key: {feature_id}") print(f"Input file: {manifest_path}") raise e def generate_feature_manifest(fd, input_file): write_fm_headers(fd) try: with open(input_file, "r", encoding="utf-8") as f: manifest = yaml.safe_load(f) validate_feature_manifest( Path(input_file).parent / FEATURE_SCHEMA, input_file, manifest ) fd.write(f"export const FeatureManifest = {json.dumps(manifest)};") except IOError as e: print(f"{input_file}: error:\n {e}\n") sys.exit(1) def platform_feature_manifest_array(features): entries = [] for feature, featureData in features.items(): # Features have to be tagged isEarlyStartup to be accessible # to Nimbus platform API if not featureData.get("isEarlyStartup", False): continue entries.extend( '{{ "{}_{}"_ns, "{}"_ns }}'.format( feature, variable, variableData["fallbackPref"] ) for (variable, variableData) in featureData.get("variables", {}).items() if variableData.get("fallbackPref", False) ) return NIMBUS_FALLBACK_PREFS.format(", ".join(entries)) def generate_platform_feature_manifest(fd, input_file): write_fm_headers(fd) def file_structure(data): return "\n".join( [ "#ifndef mozilla_NimbusFeaturesManifest_h", "#define mozilla_NimbusFeaturesManifest_h", "#include ", '#include "mozilla/Maybe.h"', '#include "nsStringFwd.h"', "namespace mozilla {", platform_feature_manifest_array(data), '#include "./lib/NimbusFeatureManifest.inc.h"', "} // namespace mozilla", "#endif // mozilla_NimbusFeaturesManifest_h", ] ) try: with open(input_file, "r", encoding="utf-8") as yaml_input: data = yaml.safe_load(yaml_input) fd.write(file_structure(data)) except IOError as e: print("{}: error:\n {}\n".format(input_file, e)) sys.exit(1)