Bug 1943612 - Provide generic machinery to load confvars.sh as a simple key/value format r=glandium

Doing so prevents users from putting customization à la moz.configure in
the confvars.sh file, which should enforce better practices.

Share the implementation with repackaging/msix.py and test it.

Differential Revision: https://phabricator.services.mozilla.com/D235769
This commit is contained in:
serge-sans-paille
2025-02-25 08:52:51 +00:00
parent 58e665f2a9
commit 69540c25b8
10 changed files with 326 additions and 19 deletions

View File

@@ -540,6 +540,46 @@ def project_flag(env=None, set_as_define=False, **kwargs):
set_define(env, option_implementation) set_define(env, option_implementation)
# A template providing a shorthand for setting a variable. The created
# option will only be settable from a confvars.sh file.
# If required, the set_as_define argument will additionally cause the variable
# to be set using set_define.
# Similarly, set_as_config can be set to False if the variable should not be
# passed to set_config.
@template
def confvar(
env=None,
set_as_config=True,
set_as_define=False,
allow_implied=False,
**kwargs,
):
if not env:
configure_error("A project_flag must be passed a variable name to set.")
if kwargs.get("nargs", 0) not in (0, 1):
configure_error("A project_flag must be passed nargs={0,1}.")
origins = ("confvars",)
if allow_implied:
origins += ("implied",)
opt = option(env=env, possible_origins=origins, **kwargs)
@depends(opt.option)
def option_implementation(value):
if value:
if len(value) == 1:
return value[0]
elif len(value):
return value
return bool(value)
if set_as_config:
set_config(env, option_implementation)
if set_as_define:
set_define(env, option_implementation)
@template @template
@imports(_from="mozbuild.configure.constants", _import="RaiseErrorOnUse") @imports(_from="mozbuild.configure.constants", _import="RaiseErrorOnUse")
def obsolete_config(name, *, replacement): def obsolete_config(name, *, replacement):

View File

@@ -440,6 +440,29 @@ pack_relative_relocs_flags = dependable(False)
include(include_project_configure) include(include_project_configure)
@depends(build_environment, build_project, "--help")
@checking("if project-specific confvars.sh exists")
# This gives access to the sandbox. Don't copy this blindly.
@imports("__sandbox__")
@imports(_from="mozbuild.configure", _import="confvars")
@imports("os.path")
def load_confvars(build_env, build_project, help):
confvars_path = os.path.join(build_env.topsrcdir, build_project, "confvars.sh")
if os.path.exists(confvars_path):
helper = __sandbox__._helper
# parse confvars
try:
keyvals = confvars.parse(confvars_path)
except confvars.ConfVarsSyntaxError as e:
die(str(e))
for key, value in keyvals.items():
# FIXME: remove test once we no longer load confvars from old-configure.
if key in __sandbox__._options:
# ~= imply_option, but with an accurate origin
helper.add(f"{key}={value}", origin="confvars", args=helper._args)
return confvars_path
# Final flags validation and gathering # Final flags validation and gathering
# ------------------------------------------------- # -------------------------------------------------

View File

@@ -0,0 +1,79 @@
# 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 mozbuild.shellutil
class ConfVarsSyntaxError(SyntaxError):
def __init__(self, msg, file, lineno, colnum, line):
super().__init__(msg, (file, lineno, colnum, line))
def parse(path):
with open(path) as confvars:
keyvals = {}
for lineno, rawline in enumerate(confvars, start=1):
line = rawline.rstrip()
# Empty line / comment.
line_no_leading_blank = line.lstrip()
if not line_no_leading_blank or line_no_leading_blank.startswith("#"):
continue
head, sym, tail = line.partition("=")
if sym != "=" or "#" in head:
raise ConfVarsSyntaxError(
"Expecting key=value format", path, lineno, 1, line
)
key = head.strip()
# Verify there's no unexpected spaces.
if key != head:
colno = 1 + line.index(key)
raise ConfVarsSyntaxError(
f"Expecting no spaces around '{key}'", path, lineno, colno, line
)
if tail.lstrip() != tail:
colno = 1 + line.index(tail)
raise ConfVarsSyntaxError(
f"Expecting no spaces between '=' and '{tail.lstrip()}'",
path,
lineno,
colno,
line,
)
# Verify we don't have duplicate keys.
if key in keyvals:
raise ConfVarsSyntaxError(
f"Invalid redefinition for '{key}'",
path,
lineno,
1 + line.index(key),
line,
)
# Parse value.
try:
values = mozbuild.shellutil.split(tail)
except mozbuild.shellutil.MetaCharacterException as e:
raise ConfVarsSyntaxError(
f"Unquoted, non-escaped special character '{e.char}'",
path,
lineno,
1 + line.index(e.char),
line,
)
except Exception as e:
raise ConfVarsSyntaxError(
e.args[0].replace(" in command", ""),
path,
lineno,
1 + line.index("="),
line,
)
value = values[0] if values else ""
# Finally, commit the key<> value pair \o/.
keyvals[key] = value
return keyvals

View File

@@ -33,6 +33,7 @@ from mozpack.mozjar import JarReader
from mozpack.packager.unpack import UnpackFinder from mozpack.packager.unpack import UnpackFinder
from six.moves import shlex_quote from six.moves import shlex_quote
from mozbuild.configure import confvars
from mozbuild.dirutils import ensureParentDir from mozbuild.dirutils import ensureParentDir
from mozbuild.repackaging.application_ini import get_application_ini_values from mozbuild.repackaging.application_ini import get_application_ini_values
@@ -208,29 +209,23 @@ def get_appconstants_sys_mjs_values(finder, *args):
def get_branding(use_official, topsrcdir, build_app, finder, log=None): def get_branding(use_official, topsrcdir, build_app, finder, log=None):
"""Figure out which branding directory to use.""" """Figure out which branding directory to use."""
conf_vars = mozpath.join(topsrcdir, build_app, "confvars.sh") confvars_path = mozpath.join(topsrcdir, build_app, "confvars.sh")
confvars_content = confvars.parse(confvars_path)
def conf_vars_value(key): for key, value in confvars_content.items():
lines = [line.strip() for line in open(conf_vars).readlines()]
for line in lines:
if line and line[0] == "#":
continue
if key not in line:
continue
_, _, value = line.partition("=")
if not value:
continue
log( log(
logging.INFO, logging.INFO,
"msix", "msix",
{"key": key, "conf_vars": conf_vars, "value": value}, {"key": key, "conf_vars": confvars_path, "value": value},
"Read '{key}' from {conf_vars}: {value}", "Read '{key}' from {conf_vars}: {value}",
) )
return value
def conf_vars_value(key):
if key in confvars_content:
return confvars_content[key]
log( log(
logging.ERROR, logging.ERROR,
"msix", "msix",
{"key": key, "conf_vars": conf_vars}, {"key": key, "conf_vars": confvars_content},
"Unable to find '{key}' in {conf_vars}!", "Unable to find '{key}' in {conf_vars}!",
) )

View File

@@ -0,0 +1,4 @@
# line comment
CONFVAR=" a b c"
OTHER_CONFVAR=d # trailing comment

View File

@@ -0,0 +1,10 @@
confvar(
"CONFVAR",
nargs=1,
help="Confvar",
)
confvar(
"OTHER_CONFVAR",
nargs=1,
help="Other confvar",
)

View File

@@ -193,5 +193,20 @@ class TestMozConfigure(BaseConfigureTest):
self.assertEqual(check_nsis_version("v3.1"), "3.1") self.assertEqual(check_nsis_version("v3.1"), "3.1")
class TestConfVars(BaseConfigureTest):
def test_loading(self):
sandbox = self.get_sandbox(
paths={},
config={},
args=[
"--enable-project=python/mozbuild/mozbuild/test/configure/data/confvars"
],
)
self.assertEqual(
list(sandbox._helper),
["CONFVAR= a b c", "OTHER_CONFVAR=d"],
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -79,6 +79,8 @@ subsuite = "mozbuild"
["test_base.py"] ["test_base.py"]
["test_confvars.py"]
["test_containers.py"] ["test_containers.py"]
["test_dotproperties.py"] ["test_dotproperties.py"]

View File

@@ -0,0 +1,139 @@
import os
import re
import unittest
from tempfile import NamedTemporaryFile
import mozunit
from mozbuild.configure.confvars import ConfVarsSyntaxError, parse
def TemporaryConfVars():
return NamedTemporaryFile("wt", delete=False)
class TestContext(unittest.TestCase):
def loads(self, *lines):
with NamedTemporaryFile("wt", delete=False) as ntf:
ntf.writelines(lines)
try:
confvars = parse(ntf.name)
finally:
os.remove(ntf.name)
return confvars
def test_parse_empty_file(self):
confvars = self.loads("# comment\n")
self.assertEqual(confvars, {})
def test_parse_simple_assignment(self):
confvars = self.loads("a=b\n")
self.assertEqual(confvars, {"a": "b"})
def test_parse_simple_assignment_with_equal_in_value(self):
confvars = self.loads("a='='\n", "b==")
self.assertEqual(confvars, {"a": "=", "b": "="})
def test_parse_simple_assignment_with_sharp_in_value(self):
confvars = self.loads("a='#'\n")
self.assertEqual(confvars, {"a": "#"})
def test_parse_simple_assignment_with_trailing_spaces(self):
confvars = self.loads("a1=1\t\n", "\n", "a2=2\n", "a3=3 \n", "a4=4")
self.assertEqual(
confvars,
{
"a1": "1",
"a2": "2",
"a3": "3",
"a4": "4",
},
)
def test_parse_trailing_comment(self):
confvars = self.loads("a=b#comment\n")
self.assertEqual(confvars, {"a": "b"})
def test_parse_invalid_assign_in_trailing_comment(self):
with self.assertRaises(ConfVarsSyntaxError) as cm:
self.loads("a#=comment\n")
self.assertTrue(
re.match("Expecting key=value format \\(.*, line 1\\)", str(cm.exception))
)
def test_parse_quoted_assignment(self):
confvars = self.loads("a='b'\n" "b=' c'\n" 'c=" \'c"\n')
self.assertEqual(confvars, {"a": "b", "b": " c", "c": " 'c"})
def test_parse_invalid_assignment(self):
with self.assertRaises(ConfVarsSyntaxError) as cm:
self.loads("a#comment\n")
self.assertTrue(
re.match("Expecting key=value format \\(.*, line 1\\)", str(cm.exception))
)
def test_parse_empty_value(self):
confvars = self.loads("a=\n")
self.assertEqual(confvars, {"a": ""})
def test_parse_invalid_value(self):
with self.assertRaises(ConfVarsSyntaxError) as cm:
self.loads("#comment\na='er\n")
self.assertTrue(
re.match(
"Unterminated quoted string \\(.*, line 2\\)",
str(cm.exception),
)
)
with self.assertRaises(ConfVarsSyntaxError) as cm:
self.loads("a= er\n")
self.assertTrue(
re.match(
"Expecting no spaces between '=' and 'er' \\(.*, line 1\\)",
str(cm.exception),
)
)
def test_parse_invalid_char(self):
with self.assertRaises(ConfVarsSyntaxError) as cm:
self.loads("a=$\n")
self.assertTrue(
re.match(
"Unquoted, non-escaped special character '\\$' \\(.*, line 1\\)",
str(cm.exception),
)
)
def test_parse_invalid_key(self):
with self.assertRaises(ConfVarsSyntaxError) as cm:
self.loads(" a=1\n")
self.assertTrue(
re.match(
"Expecting no spaces around 'a' \\(.*, line 1\\)",
str(cm.exception),
)
)
with self.assertRaises(ConfVarsSyntaxError) as cm:
self.loads("a =1\n")
self.assertTrue(
re.match(
"Expecting no spaces around 'a' \\(.*, line 1\\)",
str(cm.exception),
)
)
def test_parse_redundant_key(self):
with self.assertRaises(ConfVarsSyntaxError) as cm:
self.loads("a=1\na=2\n")
self.assertTrue(
re.match(
"Invalid redefinition for 'a' \\(.*, line 2\\)",
str(cm.exception),
)
)
if __name__ == "__main__":
mozunit.main()

View File

@@ -124,7 +124,7 @@ def all_configure_options():
# defaults. # defaults.
if ( if (
value is not None value is not None
and value.origin not in ("default", "implied") and value.origin not in ("default", "implied", "confvars")
and value != option.default and value != option.default
): ):
result.append( result.append(