python/mozbuild/mozbuild/action/file_generate.py
author Mike Shal <mshal@mozilla.com>
Wed, 09 May 2018 08:24:31 -0400
changeset 418351 1eb04a9bfb7a82eb6fac5e29be7c6b03999d9361
parent 395099 47be09a53f3df6e2beab0a8638c0f108de4454f6
child 418352 9260cc524bb54d9318075578488de8a6f40677eb
permissions -rw-r--r--
Bug 1454912 - Use a .stub file as the target for all GENERATED_FILES rules; r=nalexander The make backend was treating the first output of a GENERATED_FILES rule specially, since it was the target of the rule containing the script invocation. We want the outputs of GENERATED_FILES rules to be FileAvoidWrite so that we avoid triggering downstream rules if the outputs are unchanged, but if the target of the script invocation is FileAvoidWrite, then make may continually re-run the script during a no-op build. The solution here is to use a stub file as the target of the script invocation which will always be touched when the script runs. Since nothing else in the build depends on the stub, we don't need to FileAvoidWrite it. All actual outputs of the script can be FileAvoidWrite, and make can properly avoid work for files that haven't changed. MozReview-Commit-ID: 3GejZw2tpqu

# 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.

from __future__ import absolute_import, print_function

import argparse
import imp
import os
import sys
import traceback

from mozbuild.pythonutil import iter_modules_in_path
from mozbuild.makeutil import Makefile
from mozbuild.util import FileAvoidWrite
import buildconfig


def main(argv):
    parser = argparse.ArgumentParser('Generate a file from a Python script',
                                     add_help=False)
    parser.add_argument('--locale', metavar='locale', type=str,
                        help='The locale in use.')
    parser.add_argument('python_script', metavar='python-script', type=str,
                        help='The Python script to run')
    parser.add_argument('method_name', metavar='method-name', type=str,
                        help='The method of the script to invoke')
    parser.add_argument('output_file', metavar='output-file', type=str,
                        help='The file to generate')
    parser.add_argument('dep_file', metavar='dep-file', type=str,
                        help='File to write any additional make dependencies to')
    parser.add_argument('dep_target', metavar='dep-target', type=str,
                        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))
    with open(script, 'r') as fh:
        module = imp.load_module('script', fh, script,
                                 ('.py', 'r', imp.PY_SOURCE))
    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, mode='rb') as output:
            ret = module.__dict__[method](output, *args.additional_arguments, **kwargs)
            # 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 = 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(iter_modules_in_path(buildconfig.topsrcdir,
                                                 buildconfig.topobjdir))
                # Add dependencies on any buildconfig items that were accessed
                # by the script.
                deps |= set(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)
        # Even when our file's contents haven't changed, we want to update
        # the file's mtime so make knows this target isn't still older than
        # whatever prerequisite caused it to be built this time around.
        try:
            os.utime(args.output_file, None)
        except:
            print('Error processing file "{0}"'.format(args.output_file),
                  file=sys.stderr)
            traceback.print_exc()
    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(main(sys.argv[1:]))