1 # This Source Code Form is subject to the terms of the Mozilla Public
2 # License, v. 2.0. If a copy of the MPL was not distributed with this
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
10 from pathlib
import Path
11 from textwrap
import dedent
14 from mozboot
.mozconfig
import find_mozconfig
15 from mozpack
import path
as mozpath
17 MOZCONFIG_BAD_EXIT_CODE
= """
18 Evaluation of your mozconfig exited with an error. This could be triggered
19 by a command inside your mozconfig failing. Please change your mozconfig
20 to not error and/or to catch errors in executed commands.
23 MOZCONFIG_BAD_OUTPUT
= """
24 Evaluation of your mozconfig produced unexpected output. This could be
25 triggered by a command inside your mozconfig failing or producing some warnings
26 or error messages. Please change your mozconfig to not error and/or to catch
27 errors in executed commands.
31 class MozconfigLoadException(Exception):
32 """Raised when a mozconfig could not be loaded properly.
34 This typically indicates a malformed or misbehaving mozconfig file.
37 def __init__(self
, path
, message
, output
=None):
44 Error loading mozconfig: {path}
49 .format(path
=self
.path
, message
=message
)
60 ).format(output
="\n".join([six
.ensure_text(s
) for s
in self
.output
]))
62 Exception.__init
__(self
, message
)
65 class MozconfigLoader(object):
66 """Handles loading and parsing of mozconfig files."""
68 RE_MAKE_VARIABLE
= re
.compile(
70 ^\s* # Leading whitespace
71 (?P<var>[a-zA-Z_0-9]+) # Variable name
72 \s* [?:]?= \s* # Assignment operator surrounded by optional
74 (?P<value>.*$)""", # Everything else (likely the value)
78 IGNORE_SHELL_VARIABLES
= {"_", "BASH_ARGV", "BASH_ARGV0", "BASH_ARGC"}
80 ENVIRONMENT_VARIABLES
= {"CC", "CXX", "CFLAGS", "CXXFLAGS", "LDFLAGS", "MOZ_OBJDIR"}
84 def __init__(self
, topsrcdir
):
85 self
.topsrcdir
= topsrcdir
88 def _loader_script(self
):
89 our_dir
= os
.path
.abspath(os
.path
.dirname(__file__
))
91 return os
.path
.join(our_dir
, "mozconfig_loader")
93 def read_mozconfig(self
, path
=None):
94 """Read the contents of a mozconfig into a data structure.
96 This takes the path to a mozconfig to load. If the given path is
97 AUTODETECT, will try to find a mozconfig from the environment using
100 mozconfig files are shell scripts. So, we can't just parse them.
101 Instead, we run the shell script in a wrapper which allows us to record
102 state from execution. Thus, the output from a mozconfig is a friendly
103 static data structure.
105 if path
is self
.AUTODETECT
:
106 path
= find_mozconfig(self
.topsrcdir
)
107 if isinstance(path
, Path
):
113 "configure_args": None,
121 if "MOZ_OBJDIR" in os
.environ
:
122 result
["topobjdir"] = os
.environ
["MOZ_OBJDIR"]
125 path
= mozpath
.normsep(path
)
127 result
["configure_args"] = []
128 result
["make_extra"] = []
129 result
["make_flags"] = []
131 # Since mozconfig_loader is a shell script, running it "normally"
132 # actually leads to two shell executions on Windows. Avoid this by
133 # directly calling sh mozconfig_loader.
135 env
= dict(os
.environ
)
136 env
["PYTHONIOENCODING"] = "utf-8"
138 if "MOZILLABUILD" in os
.environ
:
139 mozillabuild
= os
.environ
["MOZILLABUILD"]
140 if (Path(mozillabuild
) / "msys2").exists():
141 shell
= mozillabuild
+ "/msys2/usr/bin/sh"
143 shell
= mozillabuild
+ "/msys/bin/sh"
144 prefer_mozillabuild_path
= [
145 os
.path
.dirname(shell
),
146 str(Path(mozillabuild
) / "bin"),
149 env
["PATH"] = os
.pathsep
.join(prefer_mozillabuild_path
)
150 if sys
.platform
== "win32":
151 shell
= shell
+ ".exe"
154 mozpath
.normsep(shell
),
155 mozpath
.normsep(self
._loader
_script
),
156 mozpath
.normsep(self
.topsrcdir
),
157 mozpath
.normsep(path
),
158 mozpath
.normsep(sys
.executable
),
159 mozpath
.join(mozpath
.dirname(self
._loader
_script
), "action", "dump_env.py"),
163 # We need to capture stderr because that's where the shell sends
164 # errors if execution fails.
165 output
= six
.ensure_text(
166 subprocess
.check_output(
168 stderr
=subprocess
.STDOUT
,
171 universal_newlines
=True,
175 except subprocess
.CalledProcessError
as e
:
176 lines
= e
.output
.splitlines()
178 # Output before actual execution shouldn't be relevant.
180 index
= lines
.index("------END_BEFORE_SOURCE")
181 lines
= lines
[index
+ 1 :]
185 raise MozconfigLoadException(path
, MOZCONFIG_BAD_EXIT_CODE
, lines
)
188 parsed
= self
._parse
_loader
_output
(output
)
189 except AssertionError:
190 # _parse_loader_output uses assertions to verify the
191 # well-formedness of the shell output; when these fail, it
192 # generally means there was a problem with the output, but we
193 # include the assertion traceback just to be sure.
194 print("Assertion failed in _parse_loader_output:")
195 traceback
.print_exc()
196 raise MozconfigLoadException(
197 path
, MOZCONFIG_BAD_OUTPUT
, output
.splitlines()
200 def diff_vars(vars_before
, vars_after
):
201 set1
= set(vars_before
.keys()) - self
.IGNORE_SHELL_VARIABLES
202 set2
= set(vars_after
.keys()) - self
.IGNORE_SHELL_VARIABLES
204 removed
= set1
- set2
205 maybe_modified
= set1
& set2
206 changed
= {"added": {}, "removed": {}, "modified": {}, "unmodified": {}}
209 changed
["added"][key
] = vars_after
[key
]
212 changed
["removed"][key
] = vars_before
[key
]
214 for key
in maybe_modified
:
215 if vars_before
[key
] != vars_after
[key
]:
216 changed
["modified"][key
] = (vars_before
[key
], vars_after
[key
])
217 elif key
in self
.ENVIRONMENT_VARIABLES
:
218 # In order for irrelevant environment variable changes not
219 # to incur in re-running configure, only a set of
220 # environment variables are stored when they are
221 # unmodified. Otherwise, changes such as using a different
222 # terminal window, or even rebooting, would trigger
224 changed
["unmodified"][key
] = vars_after
[key
]
228 result
["env"] = diff_vars(parsed
["env_before"], parsed
["env_after"])
230 # Environment variables also appear as shell variables, but that's
231 # uninteresting duplication of information. Filter them out.
233 return {k
: v
for k
, v
in x
.items() if k
not in y
}
235 result
["vars"] = diff_vars(
236 filt(parsed
["vars_before"], parsed
["env_before"]),
237 filt(parsed
["vars_after"], parsed
["env_after"]),
240 result
["configure_args"] = [self
._expand
(o
) for o
in parsed
["ac"]]
242 if "MOZ_OBJDIR" in parsed
["env_before"]:
243 result
["topobjdir"] = parsed
["env_before"]["MOZ_OBJDIR"]
245 mk
= [self
._expand
(o
) for o
in parsed
["mk"]]
248 match
= self
.RE_MAKE_VARIABLE
.match(o
)
251 result
["make_extra"].append(o
)
254 name
, value
= match
.group("var"), match
.group("value")
256 if name
== "MOZ_MAKE_FLAGS":
257 result
["make_flags"] = value
.split()
260 if name
== "MOZ_OBJDIR":
261 result
["topobjdir"] = value
262 if parsed
["env_before"].get("MOZ_PROFILE_GENERATE") == "1":
263 # If MOZ_OBJDIR is specified in the mozconfig, we need to
264 # make sure that the '/instrumented' directory gets appended
265 # for the first build to avoid an objdir mismatch when
266 # running 'mach package' on Windows.
267 result
["topobjdir"] = mozpath
.join(
268 result
["topobjdir"], "instrumented"
272 result
["make_extra"].append(o
)
276 def _parse_loader_output(self
, output
):
281 env_before_source
= {}
282 env_after_source
= {}
288 for line
in output
.splitlines():
292 if line
.startswith("------BEGIN_"):
293 assert current_type
is None
294 assert current
is None
295 assert not in_variable
296 current_type
= line
[len("------BEGIN_") :]
300 if line
.startswith("------END_"):
301 assert not in_variable
302 section
= line
[len("------END_") :]
303 assert current_type
== section
305 if current_type
== "AC_OPTION":
306 ac_options
.append("\n".join(current
))
307 elif current_type
== "MK_OPTION":
308 mk_options
.append("\n".join(current
))
314 assert current_type
is not None
317 "BEFORE_SOURCE": before_source
,
318 "AFTER_SOURCE": after_source
,
319 "ENV_BEFORE_SOURCE": env_before_source
,
320 "ENV_AFTER_SOURCE": env_after_source
,
323 if current_type
in vars_mapping
:
324 # mozconfigs are sourced using the Bourne shell (or at least
325 # in Bourne shell mode). This means |set| simply lists
326 # variables from the current shell (not functions). (Note that
327 # if Bash is installed in /bin/sh it acts like regular Bourne
328 # and doesn't print functions.) So, lines should have the
334 # The only complication is multi-line variables. Those have the
340 # TODO Bug 818377 Properly handle multi-line variables of form:
350 # Reached the end of a multi-line variable.
351 if line
.endswith("'") and not line
.endswith("\\'"):
352 current
.append(line
[:-1])
353 value
= "\n".join(current
)
359 equal_pos
= line
.find("=")
365 name
= line
[0:equal_pos
]
366 value
= line
[equal_pos
+ 1 :]
369 has_quote
= value
[0] == "'"
374 # Lines with a quote not ending in a quote are multi-line.
375 if has_quote
and not value
.endswith("'"):
377 current
.append(value
)
380 value
= value
[:-1] if has_quote
else value
382 assert name
is not None
384 vars_mapping
[current_type
][name
] = value
395 "vars_before": before_source
,
396 "vars_after": after_source
,
397 "env_before": env_before_source
,
398 "env_after": env_after_source
,
401 def _expand(self
, s
):
402 return s
.replace("@TOPSRCDIR@", self
.topsrcdir
)