1# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
2# vim: set filetype=python:
3# This Source Code Form is subject to the terms of the Mozilla Public
4# License, v. 2.0. If a copy of the MPL was not distributed with this
5# file, You can obtain one at http://mozilla.org/MPL/2.0/.
6
7
8m4 = check_prog(
9    "M4",
10    (
11        "gm4",
12        "m4",
13    ),
14)
15
16
17@depends(mozconfig)
18def prepare_mozconfig(mozconfig):
19    if mozconfig["path"]:
20        items = {}
21        for key, value in mozconfig["vars"]["added"].items():
22            items[key] = (value, "added")
23        for key, (old, value) in mozconfig["vars"]["modified"].items():
24            items[key] = (value, "modified")
25        for t in ("env", "vars"):
26            for key in mozconfig[t]["removed"].keys():
27                items[key] = (None, "removed " + t)
28        return items
29
30
31@depends("OLD_CONFIGURE", build_project)
32def old_configure(old_configure, build_project):
33    if not old_configure:
34        die("The OLD_CONFIGURE environment variable must be set")
35
36    # os.path.abspath in the sandbox will ensure forward slashes on Windows,
37    # which is actually necessary because this path actually ends up literally
38    # as $0, and backslashes there breaks autoconf's detection of the source
39    # directory.
40    old_configure = os.path.abspath(old_configure[0])
41    if build_project == "js":
42        old_configure_dir = os.path.dirname(old_configure)
43        if not old_configure_dir.endswith("/js/src"):
44            old_configure = os.path.join(
45                old_configure_dir, "js", "src", os.path.basename(old_configure)
46            )
47    return old_configure
48
49
50@depends(prepare_mozconfig, old_configure_assignments)
51@imports(_from="__builtin__", _import="open")
52@imports(_from="__builtin__", _import="print")
53@imports(_from="mozbuild.shellutil", _import="quote")
54def prepare_configure(mozconfig, old_configure_assignments):
55    with open("old-configure.vars", "w") as out:
56        log.debug("Injecting the following to old-configure:")
57
58        def inject(command):
59            print(command, file=out)  # noqa Python 2vs3
60            log.debug("| %s", command)
61
62        if mozconfig:
63            inject("# start of mozconfig values")
64            for key, (value, action) in sorted(mozconfig.items()):
65                if action.startswith("removed "):
66                    inject("unset %s # from %s" % (key, action[len("removed ") :]))
67                else:
68                    inject("%s=%s # %s" % (key, quote(value), action))
69
70            inject("# end of mozconfig values")
71
72        for k, v in old_configure_assignments:
73            inject("%s=%s" % (k, quote(v)))
74
75
76@template
77def old_configure_options(*options):
78    for opt in options:
79        option(opt, nargs="*", help="Help missing for old configure options")
80
81    @dependable
82    def all_options():
83        return list(options)
84
85    return depends(
86        host_for_sub_configure, target_for_sub_configure, all_options, *options
87    )
88
89
90@old_configure_options(
91    "--cache-file",
92    "--datadir",
93    "--enable-official-branding",
94    "--includedir",
95    "--libdir",
96    "--prefix",
97    "--with-branding",
98    "--with-distribution-id",
99    "--with-macbundlename-prefix",
100    "--x-includes",
101    "--x-libraries",
102)
103def prepare_configure_options(host, target, all_options, *options):
104    # old-configure only supports the options listed in @old_configure_options
105    # so we don't need to pass it every single option we've been passed. Only
106    # the ones that are not supported by python configure need to.
107    options = [
108        value.format(name)
109        for name, value in zip(all_options, options)
110        if value.origin != "default"
111    ] + [host, target]
112
113    return namespace(options=options, all_options=all_options)
114
115
116@template
117def old_configure_for(old_configure_path, extra_env=None):
118    if extra_env is None:
119        extra_env = dependable(None)
120
121    @depends(
122        prepare_configure,
123        prepare_configure_options,
124        altered_path,
125        extra_env,
126        build_environment,
127        old_configure_path,
128        "MOZILLABUILD",
129        awk,
130        m4,
131        shell,
132    )
133    @imports(_from="__builtin__", _import="compile")
134    @imports(_from="__builtin__", _import="open")
135    @imports(_from="__builtin__", _import="OSError")
136    @imports("glob")
137    @imports("itertools")
138    @imports("logging")
139    @imports("os")
140    @imports("subprocess")
141    @imports("sys")
142    @imports(_from="mozbuild.shellutil", _import="quote")
143    @imports(_from="mozbuild.shellutil", _import="split")
144    @imports(_from="tempfile", _import="NamedTemporaryFile")
145    @imports(_from="subprocess", _import="CalledProcessError")
146    @imports(_from="__builtin__", _import="exec")
147    def old_configure(
148        prepare_configure,
149        prepare_configure_options,
150        altered_path,
151        extra_env,
152        build_env,
153        old_configure,
154        mozillabuild,
155        awk,
156        m4,
157        shell,
158    ):
159        # Use prepare_configure to make lint happy
160        prepare_configure
161        refresh = True
162        if os.path.exists(old_configure):
163            mtime = os.path.getmtime(old_configure)
164            aclocal = os.path.join(build_env.topsrcdir, "build", "autoconf", "*.m4")
165            for input in itertools.chain(
166                (
167                    old_configure + ".in",
168                    os.path.join(os.path.dirname(old_configure), "aclocal.m4"),
169                ),
170                glob.iglob(aclocal),
171            ):
172                if os.path.getmtime(input) > mtime:
173                    break
174            else:
175                refresh = False
176
177        if refresh:
178            autoconf = os.path.join(
179                build_env.topsrcdir, "build", "autoconf", "autoconf.sh"
180            )
181            log.info("Refreshing %s with %s", old_configure, autoconf)
182            env = dict(os.environ)
183            env["M4"] = m4
184            env["AWK"] = awk
185            env["AC_MACRODIR"] = os.path.join(build_env.topsrcdir, "build", "autoconf")
186
187            try:
188                script = subprocess.check_output(
189                    [
190                        shell,
191                        autoconf,
192                        "--localdir=%s" % os.path.dirname(old_configure),
193                        old_configure + ".in",
194                    ],
195                    # Fix the working directory, so that when m4 is called, that
196                    # includes of relative paths are deterministically resolved
197                    # relative to the directory containing old-configure.
198                    cwd=os.path.dirname(old_configure),
199                    env=env,
200                )
201            except CalledProcessError as exc:
202                # Autoconf on win32 may break due to a bad $PATH.  Let the user know
203                # their $PATH is suspect.
204                if mozillabuild:
205                    mozillabuild_path = normsep(mozillabuild[0])
206                    sh_path = normsep(find_program("sh"))
207                    if mozillabuild_path not in sh_path:
208                        log.warning(
209                            "The '{}msys/bin' directory is not first in $PATH. "
210                            "This may cause autoconf to fail. ($PATH is currently "
211                            "set to: {})".format(mozillabuild_path, os.environ["PATH"])
212                        )
213                die("autoconf exited with return code {}".format(exc.returncode))
214
215            if not script:
216                die(
217                    "Generated old-configure is empty! Check that your autoconf 2.13 program works!"
218                )
219
220            # Make old-configure append to config.log, where we put our own log.
221            # This could be done with a m4 macro, but it's way easier this way
222            script = script.replace(b">./config.log", b">>${CONFIG_LOG=./config.log}")
223
224            with NamedTemporaryFile(
225                mode="wb",
226                prefix=os.path.basename(old_configure),
227                dir=os.path.dirname(old_configure),
228                delete=False,
229            ) as fh:
230                fh.write(script)
231
232            try:
233                os.rename(fh.name, old_configure)
234            except OSError:
235                try:
236                    # Likely the file already existed (on Windows). Retry after removing it.
237                    os.remove(old_configure)
238                    os.rename(fh.name, old_configure)
239                except OSError as e:
240                    die("Failed re-creating old-configure: %s" % e.message)
241
242        cmd = [shell, old_configure] + prepare_configure_options.options
243
244        env = dict(os.environ)
245
246        # For debugging purpose, in case it's not what we'd expect.
247        log.debug("Running %s", quote(*cmd))
248
249        # Our logging goes to config.log, the same file old.configure uses.
250        # We can't share the handle on the file, so close it.
251        logger = logging.getLogger("moz.configure")
252        config_log = None
253        for handler in logger.handlers:
254            if isinstance(handler, logging.FileHandler):
255                config_log = handler
256                config_log.close()
257                logger.removeHandler(config_log)
258                env["CONFIG_LOG"] = config_log.baseFilename
259                log_size = os.path.getsize(config_log.baseFilename)
260                break
261
262        if altered_path:
263            env["PATH"] = altered_path
264
265        if extra_env:
266            env.update(extra_env)
267
268        env["OLD_CONFIGURE_VARS"] = os.path.join(
269            build_env.topobjdir, "old-configure.vars"
270        )
271        proc = subprocess.Popen(
272            cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env
273        )
274        while True:
275            line = proc.stdout.readline()
276            if not line:
277                break
278            log.info(line.rstrip())
279
280        ret = proc.wait()
281        if ret:
282            with log.queue_debug():
283                if config_log:
284                    with open(config_log.baseFilename, "r") as fh:
285                        fh.seek(log_size)
286                        for line in fh:
287                            log.debug(line.rstrip())
288                log.error("old-configure failed")
289            sys.exit(ret)
290
291        if config_log:
292            # Create a new handler in append mode
293            handler = logging.FileHandler(config_log.baseFilename, mode="a", delay=True)
294            handler.setFormatter(config_log.formatter)
295            logger.addHandler(handler)
296
297        raw_config = {
298            "split": split,
299            "unique_list": unique_list,
300        }
301        with open("config.data", "r") as fh:
302            code = compile(fh.read(), "config.data", "exec")
303            exec(code, raw_config)
304
305        # Ensure all the flags known to old-configure appear in the
306        # @old_configure_options above.
307        all_options = set(prepare_configure_options.all_options)
308        for flag in raw_config["flags"]:
309            if flag not in all_options:
310                die(
311                    "Missing option in `@old_configure_options` in %s: %s",
312                    __file__,
313                    flag,
314                )
315
316        # If the code execution above fails, we want to keep the file around for
317        # debugging.
318        os.remove("config.data")
319
320        return namespace(
321            **{
322                c: [
323                    (k[1:-1], v[1:-1] if isinstance(v, str) else v)
324                    for k, v in raw_config[c]
325                ]
326                for c in ("substs", "defines")
327            }
328        )
329
330    return old_configure
331
332
333old_configure = old_configure_for(old_configure)
334set_config("OLD_CONFIGURE_SUBSTS", old_configure.substs)
335set_config("OLD_CONFIGURE_DEFINES", old_configure.defines)
336
337
338# Assuming no other option is declared after this function, handle the
339# env options that were injected by mozconfig_options by creating dummy
340# Option instances and having the sandbox's CommandLineHelper handle
341# them. We only do so for options that haven't been declared so far,
342# which should be a proxy for the options that old-configure handles
343# and that we don't know anything about.
344@depends("--help")
345@imports("__sandbox__")
346@imports(_from="mozbuild.configure.options", _import="Option")
347def remaining_mozconfig_options(_):
348    helper = __sandbox__._helper
349    for arg in list(helper):
350        if helper._origins[arg] != "mozconfig":
351            continue
352        name = arg.split("=", 1)[0]
353        if name.isupper() and name not in __sandbox__._options:
354            option = Option(env=name, nargs="*", help=name)
355            helper.handle(option)
356
357
358# Please do not add anything after remaining_mozconfig_options()
359