Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
input.py3098 linesDownload Raw Back to gyp
1# Copyright (c) 2012 Google Inc. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5
6import ast
7import multiprocessing
8import os.path
9import re
10import shlex
11import signal
12import subprocess
13import sys
14import threading
15import traceback
16
17from packaging.version import Version
18
19import gyp.common
20import gyp.simple_copy
21from gyp.common import GypError, OrderedSet
22
23# A list of types that are treated as linkable.
24linkable_types = [
25    "executable",
26    "shared_library",
27    "loadable_module",
28    "mac_kernel_extension",
29    "windows_driver",
30]
31
32# A list of sections that contain links to other targets.
33dependency_sections = ["dependencies", "export_dependent_settings"]
34
35# base_path_sections is a list of sections defined by GYP that contain
36# pathnames.  The generators can provide more keys, the two lists are merged
37# into path_sections, but you should call IsPathSection instead of using either
38# list directly.
39base_path_sections = [
40    "destination",
41    "files",
42    "include_dirs",
43    "inputs",
44    "libraries",
45    "outputs",
46    "sources",
47]
48path_sections = set()
49
50# These per-process dictionaries are used to cache build file data when loading
51# in parallel mode.
52per_process_data = {}
53per_process_aux_data = {}
54
55
56def IsPathSection(section):
57    # If section ends in one of the '=+?!' characters, it's applied to a section
58    # without the trailing characters.  '/' is notably absent from this list,
59    # because there's no way for a regular expression to be treated as a path.
60    while section and section[-1:] in "=+?!":
61        section = section[:-1]
62
63    if section in path_sections:
64        return True
65
66    # Sections matching the regexp '_(dir|file|path)s?$' are also
67    # considered PathSections. Using manual string matching since that
68    # is much faster than the regexp and this can be called hundreds of
69    # thousands of times so micro performance matters.
70    if "_" in section:
71        tail = section[-6:]
72        if tail[-1] == "s":
73            tail = tail[:-1]
74        if tail[-5:] in ("_file", "_path"):
75            return True
76        return tail[-4:] == "_dir"
77
78    return False
79
80
81# base_non_configuration_keys is a list of key names that belong in the target
82# itself and should not be propagated into its configurations.  It is merged
83# with a list that can come from the generator to
84# create non_configuration_keys.
85base_non_configuration_keys = [
86    # Sections that must exist inside targets and not configurations.
87    "actions",
88    "configurations",
89    "copies",
90    "default_configuration",
91    "dependencies",
92    "dependencies_original",
93    "libraries",
94    "postbuilds",
95    "product_dir",
96    "product_extension",
97    "product_name",
98    "product_prefix",
99    "rules",
100    "run_as",
101    "sources",
102    "standalone_static_library",
103    "suppress_wildcard",
104    "target_name",
105    "toolset",
106    "toolsets",
107    "type",
108    # Sections that can be found inside targets or configurations, but that
109    # should not be propagated from targets into their configurations.
110    "variables",
111]
112non_configuration_keys = []
113
114# Keys that do not belong inside a configuration dictionary.
115invalid_configuration_keys = [
116    "actions",
117    "all_dependent_settings",
118    "configurations",
119    "dependencies",
120    "direct_dependent_settings",
121    "libraries",
122    "link_settings",
123    "sources",
124    "standalone_static_library",
125    "target_name",
126    "type",
127]
128
129# Controls whether or not the generator supports multiple toolsets.
130multiple_toolsets = False
131
132# Paths for converting filelist paths to output paths: {
133#   toplevel,
134#   qualified_output_dir,
135# }
136generator_filelist_paths = None
137
138
139def GetIncludedBuildFiles(build_file_path, aux_data, included=None):
140    """Return a list of all build files included into build_file_path.
141
142    The returned list will contain build_file_path as well as all other files
143    that it included, either directly or indirectly.  Note that the list may
144    contain files that were included into a conditional section that evaluated
145    to false and was not merged into build_file_path's dict.
146
147    aux_data is a dict containing a key for each build file or included build
148    file.  Those keys provide access to dicts whose "included" keys contain
149    lists of all other files included by the build file.
150
151    included should be left at its default None value by external callers.  It
152    is used for recursion.
153
154    The returned list will not contain any duplicate entries.  Each build file
155    in the list will be relative to the current directory.
156    """
157
158    if included is None:
159        included = []
160
161    if build_file_path in included:
162        return included
163
164    included.append(build_file_path)
165
166    for included_build_file in aux_data[build_file_path].get("included", []):
167        GetIncludedBuildFiles(included_build_file, aux_data, included)
168
169    return included
170
171
172def CheckedEval(file_contents):
173    """Return the eval of a gyp file.
174    The gyp file is restricted to dictionaries and lists only, and
175    repeated keys are not allowed.
176    Note that this is slower than eval() is.
177    """
178
179    syntax_tree = ast.parse(file_contents)
180    assert isinstance(syntax_tree, ast.Module)
181    c1 = syntax_tree.body
182    assert len(c1) == 1
183    c2 = c1[0]
184    assert isinstance(c2, ast.Expr)
185    return CheckNode(c2.value, [])
186
187
188def CheckNode(node, keypath):
189    if isinstance(node, ast.Dict):
190        dict = {}
191        for key, value in zip(node.keys, node.values):
192            assert isinstance(key, ast.Str)
193            key = key.s
194            if key in dict:
195                raise GypError(
196                    "Key '"
197                    + key
198                    + "' repeated at level "
199                    + repr(len(keypath) + 1)
200                    + " with key path '"
201                    + ".".join(keypath)
202                    + "'"
203                )
204            kp = list(keypath)  # Make a copy of the list for descending this node.
205            kp.append(key)
206            dict[key] = CheckNode(value, kp)
207        return dict
208    elif isinstance(node, ast.List):
209        children = []
210        for index, child in enumerate(node.elts):
211            kp = list(keypath)  # Copy list.
212            kp.append(repr(index))
213            children.append(CheckNode(child, kp))
214        return children
215    elif isinstance(node, ast.Str):
216        return node.s
217    else:
218        raise TypeError(
219            "Unknown AST node at key path '" + ".".join(keypath) + "': " + repr(node)
220        )
221
222
223def LoadOneBuildFile(build_file_path, data, aux_data, includes, is_target, check):
224    if build_file_path in data:
225        return data[build_file_path]
226
227    if os.path.exists(build_file_path):
228        build_file_contents = open(build_file_path, encoding="utf-8").read()
229    else:
230        raise GypError(f"{build_file_path} not found (cwd: {os.getcwd()})")
231
232    build_file_data = None
233    try:
234        if check:
235            build_file_data = CheckedEval(build_file_contents)
236        else:
237            build_file_data = eval(build_file_contents, {"__builtins__": {}}, None)
238    except SyntaxError as e:
239        e.filename = build_file_path
240        raise
241    except Exception as e:
242        gyp.common.ExceptionAppend(e, "while reading " + build_file_path)
243        raise
244
245    if not isinstance(build_file_data, dict):
246        raise GypError("%s does not evaluate to a dictionary." % build_file_path)
247
248    data[build_file_path] = build_file_data
249    aux_data[build_file_path] = {}
250
251    # Scan for includes and merge them in.
252    if "skip_includes" not in build_file_data or not build_file_data["skip_includes"]:
253        try:
254            if is_target:
255                LoadBuildFileIncludesIntoDict(
256                    build_file_data, build_file_path, data, aux_data, includes, check
257                )
258            else:
259                LoadBuildFileIncludesIntoDict(
260                    build_file_data, build_file_path, data, aux_data, None, check
261                )
262        except Exception as e:
263            gyp.common.ExceptionAppend(
264                e, "while reading includes of " + build_file_path
265            )
266            raise
267
268    return build_file_data
269
270
271def LoadBuildFileIncludesIntoDict(
272    subdict, subdict_path, data, aux_data, includes, check
273):
274    includes_list = []
275    if includes is not None:
276        includes_list.extend(includes)
277    if "includes" in subdict:
278        for include in subdict["includes"]:
279            # "include" is specified relative to subdict_path, so compute the real
280            # path to include by appending the provided "include" to the directory
281            # in which subdict_path resides.
282            relative_include = os.path.normpath(
283                os.path.join(os.path.dirname(subdict_path), include)
284            )
285            includes_list.append(relative_include)
286        # Unhook the includes list, it's no longer needed.
287        del subdict["includes"]
288
289    # Merge in the included files.
290    for include in includes_list:
291        if "included" not in aux_data[subdict_path]:
292            aux_data[subdict_path]["included"] = []
293        aux_data[subdict_path]["included"].append(include)
294
295        gyp.DebugOutput(gyp.DEBUG_INCLUDES, "Loading Included File: '%s'", include)
296
297        MergeDicts(
298            subdict,
299            LoadOneBuildFile(include, data, aux_data, None, False, check),
300            subdict_path,
301            include,
302        )
303
304    # Recurse into subdictionaries.
305    for k, v in subdict.items():
306        if isinstance(v, dict):
307            LoadBuildFileIncludesIntoDict(v, subdict_path, data, aux_data, None, check)
308        elif isinstance(v, list):
309            LoadBuildFileIncludesIntoList(v, subdict_path, data, aux_data, check)
310
311
312# This recurses into lists so that it can look for dicts.
313def LoadBuildFileIncludesIntoList(sublist, sublist_path, data, aux_data, check):
314    for item in sublist:
315        if isinstance(item, dict):
316            LoadBuildFileIncludesIntoDict(
317                item, sublist_path, data, aux_data, None, check
318            )
319        elif isinstance(item, list):
320            LoadBuildFileIncludesIntoList(item, sublist_path, data, aux_data, check)
321
322
323# Processes toolsets in all the targets. This recurses into condition entries
324# since they can contain toolsets as well.
325def ProcessToolsetsInDict(data):
326    if "targets" in data:
327        target_list = data["targets"]
328        new_target_list = []
329        for target in target_list:
330            # If this target already has an explicit 'toolset', and no 'toolsets'
331            # list, don't modify it further.
332            if "toolset" in target and "toolsets" not in target:
333                new_target_list.append(target)
334                continue
335            if multiple_toolsets:
336                toolsets = target.get("toolsets", ["target"])
337            else:
338                toolsets = ["target"]
339            # Make sure this 'toolsets' definition is only processed once.
340            if "toolsets" in target:
341                del target["toolsets"]
342            if len(toolsets) > 0:
343                # Optimization: only do copies if more than one toolset is specified.
344                for build in toolsets[1:]:
345                    new_target = gyp.simple_copy.deepcopy(target)
346                    new_target["toolset"] = build
347                    new_target_list.append(new_target)
348                target["toolset"] = toolsets[0]
349                new_target_list.append(target)
350        data["targets"] = new_target_list
351    if "conditions" in data:
352        for condition in data["conditions"]:
353            if isinstance(condition, list):
354                for condition_dict in condition[1:]:
355                    if isinstance(condition_dict, dict):
356                        ProcessToolsetsInDict(condition_dict)
357
358
359# TODO(mark): I don't love this name.  It just means that it's going to load
360# a build file that contains targets and is expected to provide a targets dict
361# that contains the targets...
362def LoadTargetBuildFile(
363    build_file_path,
364    data,
365    aux_data,
366    variables,
367    includes,
368    depth,
369    check,
370    load_dependencies,
371):
372    # If depth is set, predefine the DEPTH variable to be a relative path from
373    # this build file's directory to the directory identified by depth.
374    if depth:
375        # TODO(dglazkov) The backslash/forward-slash replacement at the end is a
376        # temporary measure. This should really be addressed by keeping all paths
377        # in POSIX until actual project generation.
378        d = gyp.common.RelativePath(depth, os.path.dirname(build_file_path))
379        if d == "":
380            variables["DEPTH"] = "."
381        else:
382            variables["DEPTH"] = d.replace("\\", "/")
383
384    # The 'target_build_files' key is only set when loading target build files in
385    # the non-parallel code path, where LoadTargetBuildFile is called
386    # recursively.  In the parallel code path, we don't need to check whether the
387    # |build_file_path| has already been loaded, because the 'scheduled' set in
388    # ParallelState guarantees that we never load the same |build_file_path|
389    # twice.
390    if "target_build_files" in data:
391        if build_file_path in data["target_build_files"]:
392            # Already loaded.
393            return False
394        data["target_build_files"].add(build_file_path)
395
396    gyp.DebugOutput(
397        gyp.DEBUG_INCLUDES, "Loading Target Build File '%s'", build_file_path
398    )
399
400    build_file_data = LoadOneBuildFile(
401        build_file_path, data, aux_data, includes, True, check
402    )
403
404    # Store DEPTH for later use in generators.
405    build_file_data["_DEPTH"] = depth
406
407    # Set up the included_files key indicating which .gyp files contributed to
408    # this target dict.
409    if "included_files" in build_file_data:
410        raise GypError(build_file_path + " must not contain included_files key")
411
412    included = GetIncludedBuildFiles(build_file_path, aux_data)
413    build_file_data["included_files"] = []
414    for included_file in included:
415        # included_file is relative to the current directory, but it needs to
416        # be made relative to build_file_path's directory.
417        included_relative = gyp.common.RelativePath(
418            included_file, os.path.dirname(build_file_path)
419        )
420        build_file_data["included_files"].append(included_relative)
421
422    # Do a first round of toolsets expansion so that conditions can be defined
423    # per toolset.
424    ProcessToolsetsInDict(build_file_data)
425
426    # Apply "pre"/"early" variable expansions and condition evaluations.
427    ProcessVariablesAndConditionsInDict(
428        build_file_data, PHASE_EARLY, variables, build_file_path
429    )
430
431    # Since some toolsets might have been defined conditionally, perform
432    # a second round of toolsets expansion now.
433    ProcessToolsetsInDict(build_file_data)
434
435    # Look at each project's target_defaults dict, and merge settings into
436    # targets.
437    if "target_defaults" in build_file_data:
438        if "targets" not in build_file_data:
439            raise GypError("Unable to find targets in build file %s" % build_file_path)
440
441        index = 0
442        while index < len(build_file_data["targets"]):
443            # This procedure needs to give the impression that target_defaults is
444            # used as defaults, and the individual targets inherit from that.
445            # The individual targets need to be merged into the defaults.  Make
446            # a deep copy of the defaults for each target, merge the target dict
447            # as found in the input file into that copy, and then hook up the
448            # copy with the target-specific data merged into it as the replacement
449            # target dict.
450            old_target_dict = build_file_data["targets"][index]
451            new_target_dict = gyp.simple_copy.deepcopy(
452                build_file_data["target_defaults"]
453            )
454            MergeDicts(
455                new_target_dict, old_target_dict, build_file_path, build_file_path
456            )
457            build_file_data["targets"][index] = new_target_dict
458            index += 1
459
460        # No longer needed.
461        del build_file_data["target_defaults"]
462
463    # Look for dependencies.  This means that dependency resolution occurs
464    # after "pre" conditionals and variable expansion, but before "post" -
465    # in other words, you can't put a "dependencies" section inside a "post"
466    # conditional within a target.
467
468    dependencies = []
469    if "targets" in build_file_data:
470        for target_dict in build_file_data["targets"]:
471            if "dependencies" not in target_dict:
472                continue
473            for dependency in target_dict["dependencies"]:
474                dependencies.append(
475                    gyp.common.ResolveTarget(build_file_path, dependency, None)[0]
476                )
477
478    if load_dependencies:
479        for dependency in dependencies:
480            try:
481                LoadTargetBuildFile(
482                    dependency,
483                    data,
484                    aux_data,
485                    variables,
486                    includes,
487                    depth,
488                    check,
489                    load_dependencies,
490                )
491            except Exception as e:
492                gyp.common.ExceptionAppend(
493                    e, "while loading dependencies of %s" % build_file_path
494                )
495                raise
496    else:
497        return (build_file_path, dependencies)
498
499
500def CallLoadTargetBuildFile(
501    global_flags,
502    build_file_path,
503    variables,
504    includes,
505    depth,
506    check,
507    generator_input_info,
508):
509    """Wrapper around LoadTargetBuildFile for parallel processing.
510
511    This wrapper is used when LoadTargetBuildFile is executed in
512    a worker process.
513    """
514
515    try:
516        signal.signal(signal.SIGINT, signal.SIG_IGN)
517
518        # Apply globals so that the worker process behaves the same.
519        for key, value in global_flags.items():
520            globals()[key] = value
521
522        SetGeneratorGlobals(generator_input_info)
523        result = LoadTargetBuildFile(
524            build_file_path,
525            per_process_data,
526            per_process_aux_data,
527            variables,
528            includes,
529            depth,
530            check,
531            False,
532        )
533        if not result:
534            return result
535
536        (build_file_path, dependencies) = result
537
538        # We can safely pop the build_file_data from per_process_data because it
539        # will never be referenced by this process again, so we don't need to keep
540        # it in the cache.
541        build_file_data = per_process_data.pop(build_file_path)
542
543        # This gets serialized and sent back to the main process via a pipe.
544        # It's handled in LoadTargetBuildFileCallback.
545        return (build_file_path, build_file_data, dependencies)
546    except GypError as e:
547        sys.stderr.write("gyp: %s\n" % e)
548        return None
549    except Exception as e:
550        print("Exception:", e, file=sys.stderr)
551        print(traceback.format_exc(), file=sys.stderr)
552        return None
553
554
555class ParallelProcessingError(Exception):
556    pass
557
558
559class ParallelState:
560    """Class to keep track of state when processing input files in parallel.
561
562    If build files are loaded in parallel, use this to keep track of
563    state during farming out and processing parallel jobs. It's stored
564    in a global so that the callback function can have access to it.
565    """
566
567    def __init__(self):
568        # The multiprocessing pool.
569        self.pool = None
570        # The condition variable used to protect this object and notify
571        # the main loop when there might be more data to process.
572        self.condition = None
573        # The "data" dict that was passed to LoadTargetBuildFileParallel
574        self.data = None
575        # The number of parallel calls outstanding; decremented when a response
576        # was received.
577        self.pending = 0
578        # The set of all build files that have been scheduled, so we don't
579        # schedule the same one twice.
580        self.scheduled = set()
581        # A list of dependency build file paths that haven't been scheduled yet.
582        self.dependencies = []
583        # Flag to indicate if there was an error in a child process.
584        self.error = False
585
586    def LoadTargetBuildFileCallback(self, result):
587        """Handle the results of running LoadTargetBuildFile in another process."""
588        self.condition.acquire()
589        if not result:
590            self.error = True
591            self.condition.notify()
592            self.condition.release()
593            return
594        (build_file_path0, build_file_data0, dependencies0) = result
595        self.data[build_file_path0] = build_file_data0
596        self.data["target_build_files"].add(build_file_path0)
597        for new_dependency in dependencies0:
598            if new_dependency not in self.scheduled:
599                self.scheduled.add(new_dependency)
600                self.dependencies.append(new_dependency)
601        self.pending -= 1
602        self.condition.notify()
603        self.condition.release()
604
605
606def LoadTargetBuildFilesParallel(
607    build_files, data, variables, includes, depth, check, generator_input_info
608):
609    parallel_state = ParallelState()
610    parallel_state.condition = threading.Condition()
611    # Make copies of the build_files argument that we can modify while working.
612    parallel_state.dependencies = list(build_files)
613    parallel_state.scheduled = set(build_files)
614    parallel_state.pending = 0
615    parallel_state.data = data
616
617    try:
618        parallel_state.condition.acquire()
619        while parallel_state.dependencies or parallel_state.pending:
620            if parallel_state.error:
621                break
622            if not parallel_state.dependencies:
623                parallel_state.condition.wait()
624                continue
625
626            dependency = parallel_state.dependencies.pop()
627
628            parallel_state.pending += 1
629            global_flags = {
630                "path_sections": globals()["path_sections"],
631                "non_configuration_keys": globals()["non_configuration_keys"],
632                "multiple_toolsets": globals()["multiple_toolsets"],
633            }
634
635            if not parallel_state.pool:
636                parallel_state.pool = multiprocessing.Pool(multiprocessing.cpu_count())
637            parallel_state.pool.apply_async(
638                CallLoadTargetBuildFile,
639                args=(
640                    global_flags,
641                    dependency,
642                    variables,
643                    includes,
644                    depth,
645                    check,
646                    generator_input_info,
647                ),
648                callback=parallel_state.LoadTargetBuildFileCallback,
649            )
650    except KeyboardInterrupt as e:
651        parallel_state.pool.terminate()
652        raise e
653
654    parallel_state.condition.release()
655
656    parallel_state.pool.close()
657    parallel_state.pool.join()
658    parallel_state.pool = None
659
660    if parallel_state.error:
661        sys.exit(1)
662
663
664# Look for the bracket that matches the first bracket seen in a
665# string, and return the start and end as a tuple.  For example, if
666# the input is something like "<(foo <(bar)) blah", then it would
667# return (1, 13), indicating the entire string except for the leading
668# "<" and trailing " blah".
669LBRACKETS = set("{[(")
670BRACKETS = {"}": "{", "]": "[", ")": "("}
671
672
673def FindEnclosingBracketGroup(input_str):
674    stack = []
675    start = -1
676    for index, char in enumerate(input_str):
677        if char in LBRACKETS:
678            stack.append(char)
679            if start == -1:
680                start = index
681        elif char in BRACKETS:
682            if not stack:
683                return (-1, -1)
684            if stack.pop() != BRACKETS[char]:
685                return (-1, -1)
686            if not stack:
687                return (start, index + 1)
688    return (-1, -1)
689
690
691def IsStrCanonicalInt(string):
692    """Returns True if |string| is in its canonical integer form.
693
694    The canonical form is such that str(int(string)) == string.
695    """
696    if isinstance(string, str):
697        # This function is called a lot so for maximum performance, avoid
698        # involving regexps which would otherwise make the code much
699        # shorter. Regexps would need twice the time of this function.
700        if string:
701            if string == "0":
702                return True
703            if string[0] == "-":
704                string = string[1:]
705                if not string:
706                    return False
707            if "1" <= string[0] <= "9":
708                return string.isdigit()
709
710    return False
711
712
713# This matches things like "<(asdf)", "<!(cmd)", "<!@(cmd)", "<|(list)",
714# "<!interpreter(arguments)", "<([list])", and even "<([)" and "<(<())".
715# In the last case, the inner "<()" is captured in match['content'].
716early_variable_re = re.compile(
717    r"(?P<replace>(?P<type><(?:(?:!?@?)|\|)?)"
718    r"(?P<command_string>[-a-zA-Z0-9_.]+)?"
719    r"\((?P<is_array>\s*\[?)"
720    r"(?P<content>.*?)(\]?)\))"
721)
722
723# This matches the same as early_variable_re, but with '>' instead of '<'.
724late_variable_re = re.compile(
725    r"(?P<replace>(?P<type>>(?:(?:!?@?)|\|)?)"
726    r"(?P<command_string>[-a-zA-Z0-9_.]+)?"
727    r"\((?P<is_array>\s*\[?)"
728    r"(?P<content>.*?)(\]?)\))"
729)
730
731# This matches the same as early_variable_re, but with '^' instead of '<'.
732latelate_variable_re = re.compile(
733    r"(?P<replace>(?P<type>[\^](?:(?:!?@?)|\|)?)"
734    r"(?P<command_string>[-a-zA-Z0-9_.]+)?"
735    r"\((?P<is_array>\s*\[?)"
736    r"(?P<content>.*?)(\]?)\))"
737)
738
739# Global cache of results from running commands so they don't have to be run
740# more then once.
741cached_command_results = {}
742
743
744def FixupPlatformCommand(cmd):
745    if sys.platform == "win32":
746        if isinstance(cmd, list):
747            cmd = [re.sub("^cat ", "type ", cmd[0])] + cmd[1:]
748        else:
749            cmd = re.sub("^cat ", "type ", cmd)
750    return cmd
751
752
753PHASE_EARLY = 0
754PHASE_LATE = 1
755PHASE_LATELATE = 2
756
757
758def ExpandVariables(input, phase, variables, build_file):
759    # Look for the pattern that gets expanded into variables
760    if phase == PHASE_EARLY:
761        variable_re = early_variable_re
762        expansion_symbol = "<"
763    elif phase == PHASE_LATE:
764        variable_re = late_variable_re
765        expansion_symbol = ">"
766    elif phase == PHASE_LATELATE:
767        variable_re = latelate_variable_re
768        expansion_symbol = "^"
769    else:
770        assert False
771
772    input_str = str(input)
773    if IsStrCanonicalInt(input_str):
774        return int(input_str)
775
776    # Do a quick scan to determine if an expensive regex search is warranted.
777    if expansion_symbol not in input_str:
778        return input_str
779
780    # Get the entire list of matches as a list of MatchObject instances.
781    # (using findall here would return strings instead of MatchObjects).
782    matches = list(variable_re.finditer(input_str))
783    if not matches:
784        return input_str
785
786    output = input_str
787    # Reverse the list of matches so that replacements are done right-to-left.
788    # That ensures that earlier replacements won't mess up the string in a
789    # way that causes later calls to find the earlier substituted text instead
790    # of what's intended for replacement.
791    matches.reverse()
792    for match_group in matches:
793        match = match_group.groupdict()
794        gyp.DebugOutput(gyp.DEBUG_VARIABLES, "Matches: %r", match)
795        # match['replace'] is the substring to look for, match['type']
796        # is the character code for the replacement type (< > <! >! <| >| <@
797        # >@ <!@ >!@), match['is_array'] contains a '[' for command
798        # arrays, and match['content'] is the name of the variable (< >)
799        # or command to run (<! >!). match['command_string'] is an optional
800        # command string. Currently, only 'pymod_do_main' is supported.
801
802        # run_command is true if a ! variant is used.
803        run_command = "!" in match["type"]
804        command_string = match["command_string"]
805
806        # file_list is true if a | variant is used.
807        file_list = "|" in match["type"]
808
809        # Capture these now so we can adjust them later.
810        replace_start = match_group.start("replace")
811        replace_end = match_group.end("replace")
812
813        # Find the ending paren, and re-evaluate the contained string.
814        (c_start, c_end) = FindEnclosingBracketGroup(input_str[replace_start:])
815
816        # Adjust the replacement range to match the entire command
817        # found by FindEnclosingBracketGroup (since the variable_re
818        # probably doesn't match the entire command if it contained
819        # nested variables).
820        replace_end = replace_start + c_end
821
822        # Find the "real" replacement, matching the appropriate closing
823        # paren, and adjust the replacement start and end.
824        replacement = input_str[replace_start:replace_end]
825
826        # Figure out what the contents of the variable parens are.
827        contents_start = replace_start + c_start + 1
828        contents_end = replace_end - 1
829        contents = input_str[contents_start:contents_end]
830
831        # Do filter substitution now for <|().
832        # Admittedly, this is different than the evaluation order in other
833        # contexts. However, since filtration has no chance to run on <|(),
834        # this seems like the only obvious way to give them access to filters.
835        if file_list:
836            processed_variables = gyp.simple_copy.deepcopy(variables)
837            ProcessListFiltersInDict(contents, processed_variables)
838            # Recurse to expand variables in the contents
839            contents = ExpandVariables(contents, phase, processed_variables, build_file)
840        else:
841            # Recurse to expand variables in the contents
842            contents = ExpandVariables(contents, phase, variables, build_file)
843
844        # Strip off leading/trailing whitespace so that variable matches are
845        # simpler below (and because they are rarely needed).
846        contents = contents.strip()
847
848        # expand_to_list is true if an @ variant is used.  In that case,
849        # the expansion should result in a list.  Note that the caller
850        # is to be expecting a list in return, and not all callers do
851        # because not all are working in list context.  Also, for list
852        # expansions, there can be no other text besides the variable
853        # expansion in the input string.
854        expand_to_list = "@" in match["type"] and input_str == replacement
855
856        if run_command or file_list:
857            # Find the build file's directory, so commands can be run or file lists
858            # generated relative to it.
859            build_file_dir = os.path.dirname(build_file)
860            if build_file_dir == "" and not file_list:
861                # If build_file is just a leaf filename indicating a file in the
862                # current directory, build_file_dir might be an empty string.  Set
863                # it to None to signal to subprocess.Popen that it should run the
864                # command in the current directory.
865                build_file_dir = None
866
867        # Support <|(listfile.txt ...) which generates a file
868        # containing items from a gyp list, generated at gyp time.
869        # This works around actions/rules which have more inputs than will
870        # fit on the command line.
871        if file_list:
872            contents_list = (
873                contents if isinstance(contents, list) else contents.split(" ")
874            )
875            replacement = contents_list[0]
876            if os.path.isabs(replacement):
877                raise GypError('| cannot handle absolute paths, got "%s"' % replacement)
878
879            if not generator_filelist_paths:
880                path = os.path.join(build_file_dir, replacement)
881            else:
882                if os.path.isabs(build_file_dir):
883                    toplevel = generator_filelist_paths["toplevel"]
884                    rel_build_file_dir = gyp.common.RelativePath(
885                        build_file_dir, toplevel
886                    )
887                else:
888                    rel_build_file_dir = build_file_dir
889                qualified_out_dir = generator_filelist_paths["qualified_out_dir"]
890                path = os.path.join(qualified_out_dir, rel_build_file_dir, replacement)
891                gyp.common.EnsureDirExists(path)
892
893            replacement = gyp.common.RelativePath(path, build_file_dir)
894            f = gyp.common.WriteOnDiff(path)
895            for i in contents_list[1:]:
896                f.write("%s\n" % i)
897            f.close()
898
899        elif run_command:
900            use_shell = True
901            if match["is_array"]:
902                contents = eval(contents)
903                use_shell = False
904
905            # Check for a cached value to avoid executing commands, or generating
906            # file lists more than once. The cache key contains the command to be
907            # run as well as the directory to run it from, to account for commands
908            # that depend on their current directory.
909            # TODO(http://code.google.com/p/gyp/issues/detail?id=111): In theory,
910            # someone could author a set of GYP files where each time the command
911            # is invoked it produces different output by design. When the need
912            # arises, the syntax should be extended to support no caching off a
913            # command's output so it is run every time.
914            cache_key = (str(contents), build_file_dir)
915            cached_value = cached_command_results.get(cache_key, None)
916            if cached_value is None:
917                gyp.DebugOutput(
918                    gyp.DEBUG_VARIABLES,
919                    "Executing command '%s' in directory '%s'",
920                    contents,
921                    build_file_dir,
922                )
923
924                replacement = ""
925
926                if command_string == "pymod_do_main":
927                    # <!pymod_do_main(modulename param eters) loads |modulename| as a
928                    # python module and then calls that module's DoMain() function,
929                    # passing ["param", "eters"] as a single list argument. For modules
930                    # that don't load quickly, this can be faster than
931                    # <!(python modulename param eters). Do this in |build_file_dir|.
932                    oldwd = os.getcwd()  # Python doesn't like os.open('.'): no fchdir.
933                    if build_file_dir:  # build_file_dir may be None (see above).
934                        os.chdir(build_file_dir)
935                    sys.path.append(os.getcwd())
936                    try:
937                        parsed_contents = shlex.split(contents)
938                        try:
939                            py_module = __import__(parsed_contents[0])
940                        except ImportError as e:
941                            raise GypError(
942                                "Error importing pymod_do_main"
943                                "module (%s): %s" % (parsed_contents[0], e)
944                            )
945                        replacement = str(
946                            py_module.DoMain(parsed_contents[1:])
947                        ).rstrip()
948                    finally:
949                        sys.path.pop()
950                        os.chdir(oldwd)
951                    assert replacement is not None
952                elif command_string:
953                    raise GypError(
954                        "Unknown command string '%s' in '%s'."
955                        % (command_string, contents)
956                    )
957                else:
958                    # Fix up command with platform specific workarounds.
959                    contents = FixupPlatformCommand(contents)
960                    try:
961                        # stderr will be printed no matter what
962                        result = subprocess.run(
963                            contents,
964                            stdout=subprocess.PIPE,
965                            shell=use_shell,
966                            cwd=build_file_dir,
967                            check=False,
968                        )
969                    except Exception as e:
970                        raise GypError(
971                            "%s while executing command '%s' in %s"
972                            % (e, contents, build_file)
973                        )
974
975                    if result.returncode > 0:
976                        raise GypError(
977                            "Call to '%s' returned exit status %d while in %s."
978                            % (contents, result.returncode, build_file)
979                        )
980                    replacement = result.stdout.decode("utf-8").rstrip()
981
982                cached_command_results[cache_key] = replacement
983            else:
984                gyp.DebugOutput(
985                    gyp.DEBUG_VARIABLES,
986                    "Had cache value for command '%s' in directory '%s'",
987                    contents,
988                    build_file_dir,
989                )
990                replacement = cached_value
991
992        elif contents not in variables:
993            if contents[-1] in ["!", "/"]:
994                # In order to allow cross-compiles (nacl) to happen more naturally,
995                # we will allow references to >(sources/) etc. to resolve to
996                # and empty list if undefined. This allows actions to:
997                # 'action!': [
998                #   '>@(_sources!)',
999                # ],
1000                # 'action/': [
1001                #   '>@(_sources/)',
1002                # ],
1003                replacement = []
1004            else:
1005                raise GypError("Undefined variable " + contents + " in " + build_file)
1006        else:
1007            replacement = variables[contents]
1008
1009        if isinstance(replacement, bytes) and not isinstance(replacement, str):
1010            replacement = replacement.decode("utf-8")  # done on Python 3 only
1011        if isinstance(replacement, list):
1012            for item in replacement:
1013                if isinstance(item, bytes) and not isinstance(item, str):
1014                    item = item.decode("utf-8")  # done on Python 3 only
1015                if not contents[-1] == "/" and type(item) not in (str, int):
1016                    raise GypError(
1017                        "Variable "
1018                        + contents
1019                        + " must expand to a string or list of strings; "
1020                        + "list contains a "
1021                        + item.__class__.__name__
1022                    )
1023            # Run through the list and handle variable expansions in it.  Since
1024            # the list is guaranteed not to contain dicts, this won't do anything
1025            # with conditions sections.
1026            ProcessVariablesAndConditionsInList(
1027                replacement, phase, variables, build_file
1028            )
1029        elif type(replacement) not in (str, int):
1030            raise GypError(
1031                "Variable "
1032                + contents
1033                + " must expand to a string or list of strings; "
1034                + "found a "
1035                + replacement.__class__.__name__
1036            )
1037
1038        if expand_to_list:
1039            # Expanding in list context.  It's guaranteed that there's only one
1040            # replacement to do in |input_str| and that it's this replacement.  See
1041            # above.
1042            if isinstance(replacement, list):
1043                # If it's already a list, make a copy.
1044                output = replacement[:]
1045            else:
1046                # Split it the same way sh would split arguments.
1047                output = shlex.split(str(replacement))
1048        else:
1049            # Expanding in string context.
1050            encoded_replacement = ""
1051            if isinstance(replacement, list):
1052                # When expanding a list into string context, turn the list items
1053                # into a string in a way that will work with a subprocess call.
1054                #
1055                # TODO(mark): This isn't completely correct.  This should
1056                # call a generator-provided function that observes the
1057                # proper list-to-argument quoting rules on a specific
1058                # platform instead of just calling the POSIX encoding
1059                # routine.
1060                encoded_replacement = gyp.common.EncodePOSIXShellList(replacement)
1061            else:
1062                encoded_replacement = replacement
1063
1064            output = (
1065                output[:replace_start] + str(encoded_replacement) + output[replace_end:]
1066            )
1067        # Prepare for the next match iteration.
1068        input_str = output
1069
1070    if output == input:
1071        gyp.DebugOutput(
1072            gyp.DEBUG_VARIABLES,
1073            "Found only identity matches on %r, avoiding infinite recursion.",
1074            output,
1075        )
1076    else:
1077        # Look for more matches now that we've replaced some, to deal with
1078        # expanding local variables (variables defined in the same
1079        # variables block as this one).
1080        gyp.DebugOutput(gyp.DEBUG_VARIABLES, "Found output %r, recursing.", output)
1081        if isinstance(output, list):
1082            if output and isinstance(output[0], list):
1083                # Leave output alone if it's a list of lists.
1084                # We don't want such lists to be stringified.
1085                pass
1086            else:
1087                new_output = []
1088                for item in output:
1089                    new_output.append(
1090                        ExpandVariables(item, phase, variables, build_file)
1091                    )
1092                output = new_output
1093        else:
1094            output = ExpandVariables(output, phase, variables, build_file)
1095
1096    # Convert all strings that are canonically-represented integers into integers.
1097    if isinstance(output, list):
1098        for index, outstr in enumerate(output):
1099            if IsStrCanonicalInt(outstr):
1100                output[index] = int(outstr)
1101    elif IsStrCanonicalInt(output):
1102        output = int(output)
1103
1104    return output
1105
1106
1107# The same condition is often evaluated over and over again so it
1108# makes sense to cache as much as possible between evaluations.
1109cached_conditions_asts = {}
1110
1111
1112def EvalCondition(condition, conditions_key, phase, variables, build_file):
1113    """Returns the dict that should be used or None if the result was
1114    that nothing should be used."""
1115    if not isinstance(condition, list):
1116        raise GypError(conditions_key + " must be a list")
1117    if len(condition) < 2:
1118        # It's possible that condition[0] won't work in which case this
1119        # attempt will raise its own IndexError.  That's probably fine.
1120        raise GypError(
1121            conditions_key
1122            + " "
1123            + condition[0]
1124            + " must be at least length 2, not "
1125            + str(len(condition))
1126        )
1127
1128    i = 0
1129    result = None
1130    while i < len(condition):
1131        cond_expr = condition[i]
1132        true_dict = condition[i + 1]
1133        if not isinstance(true_dict, dict):
1134            raise GypError(
1135                f"{conditions_key} {cond_expr} must be followed by a dictionary, "
1136                f"not {type(true_dict)}"
1137            )
1138        if len(condition) > i + 2 and isinstance(condition[i + 2], dict):
1139            false_dict = condition[i + 2]
1140            i = i + 3
1141            if i != len(condition):
1142                raise GypError(
1143                    f"{conditions_key} {cond_expr} has "
1144                    f"{len(condition) - i} unexpected trailing items"
1145                )
1146        else:
1147            false_dict = None
1148            i = i + 2
1149        if result is None:
1150            result = EvalSingleCondition(
1151                cond_expr, true_dict, false_dict, phase, variables, build_file
1152            )
1153
1154    return result
1155
1156
1157def EvalSingleCondition(cond_expr, true_dict, false_dict, phase, variables, build_file):
1158    """Returns true_dict if cond_expr evaluates to true, and false_dict
1159    otherwise."""
1160    # Do expansions on the condition itself.  Since the condition can naturally
1161    # contain variable references without needing to resort to GYP expansion
1162    # syntax, this is of dubious value for variables, but someone might want to
1163    # use a command expansion directly inside a condition.
1164    cond_expr_expanded = ExpandVariables(cond_expr, phase, variables, build_file)
1165    if type(cond_expr_expanded) not in (str, int):
1166        raise ValueError(
1167            "Variable expansion in this context permits str and int "
1168            + "only, found "
1169            + cond_expr_expanded.__class__.__name__
1170        )
1171
1172    try:
1173        if cond_expr_expanded in cached_conditions_asts:
1174            ast_code = cached_conditions_asts[cond_expr_expanded]
1175        else:
1176            ast_code = compile(cond_expr_expanded, "<string>", "eval")
1177            cached_conditions_asts[cond_expr_expanded] = ast_code
1178        env = {"__builtins__": {}, "v": Version}
1179        if eval(ast_code, env, variables):
1180            return true_dict
1181        return false_dict
1182    except SyntaxError as e:
1183        syntax_error = SyntaxError(
1184            "%s while evaluating condition '%s' in %s "
1185            "at character %d." % (str(e.args[0]), e.text, build_file, e.offset),
1186            e.filename,
1187            e.lineno,
1188            e.offset,
1189            e.text,
1190        )
1191        raise syntax_error
1192    except NameError as e:
1193        gyp.common.ExceptionAppend(
1194            e,
1195            f"while evaluating condition '{cond_expr_expanded}' in {build_file}",
1196        )
1197        raise GypError(e)
1198
1199
1200def ProcessConditionsInDict(the_dict, phase, variables, build_file):

Showing the first 1,200 of 3098 lines. Download the file for the rest.

codekingpro/portable-devtools · Team Ai