summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozbuild/action/file_generate.py
blob: 98dec4e3599cc8d4ca8e15816daa58288e19a701 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# 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/.

# Given a Python script and arguments describing the output file, and
# the arguments that can be used to generate the output file, call the
# script's |main| method with appropriate arguments.

import argparse
import importlib.util
import os
import sys
import traceback

import buildconfig
import six

from mozbuild.action.util import log_build_task
from mozbuild.makeutil import Makefile
from mozbuild.pythonutil import iter_modules_in_path
from mozbuild.util import FileAvoidWrite


def main(argv):
    parser = argparse.ArgumentParser(
        "Generate a file from a Python script", add_help=False
    )
    parser.add_argument(
        "--locale", metavar="locale", type=six.text_type, help="The locale in use."
    )
    parser.add_argument(
        "python_script",
        metavar="python-script",
        type=six.text_type,
        help="The Python script to run",
    )
    parser.add_argument(
        "method_name",
        metavar="method-name",
        type=six.text_type,
        help="The method of the script to invoke",
    )
    parser.add_argument(
        "output_file",
        metavar="output-file",
        type=six.text_type,
        help="The file to generate",
    )
    parser.add_argument(
        "dep_file",
        metavar="dep-file",
        type=six.text_type,
        help="File to write any additional make dependencies to",
    )
    parser.add_argument(
        "dep_target",
        metavar="dep-target",
        type=six.text_type,
        help="Make target to use in the dependencies file",
    )
    parser.add_argument(
        "additional_arguments",
        metavar="arg",
        nargs=argparse.REMAINDER,
        help="Additional arguments to the script's main() method",
    )

    args = parser.parse_args(argv)

    kwargs = {}
    if args.locale:
        kwargs["locale"] = args.locale
    script = args.python_script
    # Permit the script to import modules from the same directory in which it
    # resides.  The justification for doing this is that if we were invoking
    # the script as:
    #
    #    python script arg1...
    #
    # then importing modules from the script's directory would come for free.
    # Since we're invoking the script in a roundabout way, we provide this
    # bit of convenience.
    sys.path.append(os.path.dirname(script))
    spec = importlib.util.spec_from_file_location("script", script)
    module = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(module)
    method = args.method_name
    if not hasattr(module, method):
        print(
            'Error: script "{0}" is missing a {1} method'.format(script, method),
            file=sys.stderr,
        )
        return 1

    ret = 1
    try:
        with FileAvoidWrite(args.output_file, readmode="rb") as output:
            try:
                ret = module.__dict__[method](
                    output, *args.additional_arguments, **kwargs
                )
            except Exception:
                # Ensure that we don't overwrite the file if the script failed.
                output.avoid_writing_to_file()
                raise

            # The following values indicate a statement of success:
            #  - a set() (see below)
            #  - 0
            #  - False
            #  - None
            #
            # Everything else is an error (so scripts can conveniently |return
            # 1| or similar). If a set is returned, the elements of the set
            # indicate additional dependencies that will be listed in the deps
            # file. Python module imports are automatically included as
            # dependencies.
            if isinstance(ret, set):
                deps = set(six.ensure_text(s) for s in ret)
                # The script succeeded, so reset |ret| to indicate that.
                ret = None
            else:
                deps = set()

            # Only write out the dependencies if the script was successful
            if not ret:
                # Add dependencies on any python modules that were imported by
                # the script.
                deps |= set(
                    six.ensure_text(s)
                    for s in iter_modules_in_path(
                        buildconfig.topsrcdir, buildconfig.topobjdir
                    )
                )
                # Add dependencies on any buildconfig items that were accessed
                # by the script.
                deps |= set(six.ensure_text(s) for s in buildconfig.get_dependencies())

                mk = Makefile()
                mk.create_rule([args.dep_target]).add_dependencies(deps)
                with FileAvoidWrite(args.dep_file) as dep_file:
                    mk.dump(dep_file)
            else:
                # Ensure that we don't overwrite the file if the script failed.
                output.avoid_writing_to_file()

    except IOError as e:
        print('Error opening file "{0}"'.format(e.filename), file=sys.stderr)
        traceback.print_exc()
        return 1
    return ret


if __name__ == "__main__":
    sys.exit(log_build_task(main, sys.argv[1:]))