248 lines
9.2 KiB
Python
248 lines
9.2 KiB
Python
# 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
|
|
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 = (
|
|
"constexpr std::pair<nsLiteralCString, nsLiteralCString>"
|
|
"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 = {
|
|
"aboutwelcome",
|
|
"bounceTrackingProtection",
|
|
"newtab",
|
|
"pocketNewtab",
|
|
"preonboarding",
|
|
"testFeature",
|
|
"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"
|
|
),
|
|
}
|
|
|
|
|
|
def write_fm_headers(fd):
|
|
fd.write(HEADER_LINE)
|
|
|
|
|
|
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 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")
|
|
|
|
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):
|
|
write_fm_headers(fd)
|
|
|
|
try:
|
|
with open(input_file, 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 OSError 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 <utility>",
|
|
'#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, encoding="utf-8") as yaml_input:
|
|
data = yaml.safe_load(yaml_input)
|
|
fd.write(file_structure(data))
|
|
except OSError as e:
|
|
print(f"{input_file}: error:\n {e}\n")
|
|
sys.exit(1)
|