Source code
Revision control
Copy as Markdown
Other Tools
# 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,
import json
import sys
from pathlib import Path
from urllib.parse import urlparse
import jsonschema
import yaml
from jsonschema.validators import validator_for
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 = (
"constinit std::array NIMBUS_FALLBACK_PREFS = "
"std::to_array<std::pair<nsLiteralCString, nsLiteralCString>>({{\n"
"{entries}\n"
"}});"
)
NIMBUS_PLATFORM_FEATURES = (
"#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED\n"
"constinit static std::array NIMBUS_PLATFORM_FEATURES = "
"std::to_array<std::tuple<nsLiteralCString, bool>>({{\n"
"{entries}\n"
"}});\n"
"#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED\n"
)
# Do not add new feature IDs to this list! isEarlyStartup is being deprecated.
#
# In most cases new features should use setPref to read feature values from
# platform code.
#
# However, if the platform feature API is required, then the feature can be
# added to this list.
#
ALLOWED_ISEARLYSTARTUP_FEATURE_IDS = {
"aboutwelcome",
"bounceTrackingProtection",
"newtab",
"opaqueResponseBlocking", # Requires C++ API for exposure events
"pocketNewtab",
"preonboarding",
"testFeature", # Test-only feature
"testPlatformExposureFeature", # Test-only feature
"upgradeDialog",
}
DISALLOWED_PREFS = {
# Disabling either of these prefs will cause unenrollment from all active
# enrollments, which will then cause the pref to reset.
"app.shield.optoutstudies.enabled": (
"disabling Nimbus causes immediate unenrollment"
),
"datareporting.healthreport.uploadEnabled": (
"disabling telemetry causes immediate unenrollment"
),
# Changing the Remote Settings endpoint will cause unenrollment from the recipe.
"services.settings.server": (
"changing the Remote Settings endpoint will break clients"
),
"messaging-system.rsexperimentloader.collection": (
"changing the Nimbus collection will break clients"
),
"nimbus.debug": "internal Nimbus preference for QA",
# This pref controls the return value of xpc::IsInAutomation(), which is
# used by code to check if we are in a test.
"security.turn_off_all_security_so_that_viruses_can_take_over_this_computer": (
"this pref is automation-only and is unsafe to enable outside tests"
),
"security.sandbox.content.level": (
"changing this value can lower the security of clients"
),
}
def validate_feature_manifest(schema_path, manifest_path, manifest):
TOPSRCDIR = Path(__file__).parent.parent.parent.parent.parent
with open(schema_path) 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 new isEarlyStartup features should not be added"
)
print(
"Either remove isEarlyStartup: true or add to the override list in "
"generate_feature_manifest.py"
)
print(
)
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")
if is_early_startup and feature.get("allowCoenrollment", False):
raise Exception(
"A feature cannot use isEarlyStartup and allowCoenrollment"
)
for variable, variable_def in feature.get("variables", {}).items():
set_pref = variable_def.get("setPref", {}).get("pref")
if set_pref is None:
continue
if reason := DISALLOWED_PREFS.get(set_pref):
raise Exception(
f"Pref {set_pref} cannot be controlled by Nimbus: {reason}"
)
if feature.get("allowCoenrollment", False):
raise Exception("A feature cannot use co-enrollment and setPref")
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")
if "schema" in feature:
schema_path = TOPSRCDIR / feature["schema"]["path"]
if not schema_path.exists():
raise Exception(f"Schema does not exist at {schema_path}")
with schema_path.open() as f:
try:
schema_contents = json.load(f)
except Exception as e:
raise Exception(f"Cannot parse schema for {feature_id}") from e
try:
validator_for(schema_contents).check_schema(schema_contents)
except Exception as e:
raise Exception(f"Invalid schema for {feature_id}") from e
uri = urlparse(feature["schema"]["uri"])
if uri.scheme not in ("resource", "chrome"):
raise Exception(
"Only resource:// and chrome:// URIs are supported for schemas"
)
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):
try:
with open(input_file, encoding="utf-8") as f:
manifest = yaml.safe_load(f)
except OSError as e:
print(f"{input_file}: error:\n {e}\n")
sys.exit(1)
validate_feature_manifest(
Path(input_file).parent / FEATURE_SCHEMA, input_file, manifest
)
contents = (
f"{HEADER_LINE}\n"
f"export const FeatureManifest = {json.dumps(manifest, indent=2)}"
)
try:
fd.write(contents)
except OSError as e:
print(f"{input_file}: error:\n {e}\n")
sys.exit(1)
def generate_platform_feature_array(features):
def render_bool(b):
if b:
return "true"
return "false"
# Features have to be tagged isEarlyStartup to be accessible
# to Nimbus platform API
entries = [
f' std::make_tuple("{feature_id}"_ns, {render_bool(feature_data["hasExposure"])})'
for (feature_id, feature_data) in features.items()
if feature_data.get("isEarlyStartup", False)
]
return NIMBUS_PLATFORM_FEATURES.format(entries=",\n".join(entries))
def generate_platform_fallback_pref_array(features):
entries = [
f' {{ "{feature_id}_{variable}"_ns, "{variable_data["fallbackPref"]}"_ns }}'
for (feature_id, feature_data) in features.items()
if feature_data.get("isEarlyStartup", False)
for (variable, variable_data) in feature_data.get("variables", {}).items()
if variable_data.get("fallbackPref", False)
]
return NIMBUS_FALLBACK_PREFS.format(entries=",\n".join(entries))
def generate_platform_feature_manifest(fd, input_file):
try:
with open(input_file, encoding="utf-8") as yaml_input:
data = yaml.safe_load(yaml_input)
except OSError as e:
print(f"{input_file}: error:\n {e}\n")
sys.exit(1)
contents = "\n".join([
HEADER_LINE,
"#ifndef mozilla_NimbusFeatureManifest_h",
"#define mozilla_NimbusFeatureManifest_h",
"",
"#include <array>",
"#include <utility>",
"",
'#include "mozilla/Maybe.h"',
'#include "nsStringFwd.h"',
"",
"namespace mozilla::nimbus {",
"",
f"{generate_platform_feature_array(data)}",
"",
f"{generate_platform_fallback_pref_array(data)}",
"",
"} // namespace mozilla::nimbus",
"",
"#endif // mozilla_NimbusFeatureManifest_h\n",
])
try:
fd.write(contents)
except OSError as e:
print(f"{input_file}: error:\n {e}\n")
sys.exit(1)