xref: /aosp_15_r20/external/cronet/build/action_helpers.py (revision 6777b5387eb2ff775bb5750e3f5d96f37fb7352b)
1# Copyright 2023 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4"""Helper functions useful when writing scripts used by action() targets."""
5
6import contextlib
7import filecmp
8import os
9import pathlib
10import posixpath
11import shutil
12import tempfile
13
14import gn_helpers
15
16from typing import Optional
17from typing import Sequence
18
19
20@contextlib.contextmanager
21def atomic_output(path, mode='w+b', only_if_changed=True):
22  """Prevent half-written files and dirty mtimes for unchanged files.
23
24  Args:
25    path: Path to the final output file, which will be written atomically.
26    mode: The mode to open the file in (str).
27    only_if_changed: Whether to maintain the mtime if the file has not changed.
28  Returns:
29    A Context Manager that yields a NamedTemporaryFile instance. On exit, the
30    manager will check if the file contents is different from the destination
31    and if so, move it into place.
32
33  Example:
34    with action_helpers.atomic_output(output_path) as tmp_file:
35      subprocess.check_call(['prog', '--output', tmp_file.name])
36  """
37  # Create in same directory to ensure same filesystem when moving.
38  dirname = os.path.dirname(path) or '.'
39  os.makedirs(dirname, exist_ok=True)
40  with tempfile.NamedTemporaryFile(mode,
41                                   suffix=os.path.basename(path),
42                                   dir=dirname,
43                                   delete=False) as f:
44    try:
45      yield f
46
47      # File should be closed before comparison/move.
48      f.close()
49      if not (only_if_changed and os.path.exists(path)
50              and filecmp.cmp(f.name, path)):
51        shutil.move(f.name, path)
52    finally:
53      f.close()
54      if os.path.exists(f.name):
55        os.unlink(f.name)
56
57
58def add_depfile_arg(parser):
59  if hasattr(parser, 'add_option'):
60    func = parser.add_option
61  else:
62    func = parser.add_argument
63  func('--depfile', help='Path to depfile (refer to "gn help depfile")')
64
65
66def write_depfile(depfile_path: str,
67                  first_gn_output: str,
68                  inputs: Optional[Sequence[str]] = None) -> None:
69  """Writes a ninja depfile.
70
71  See notes about how to use depfiles in //build/docs/writing_gn_templates.md.
72
73  Args:
74    depfile_path: Path to file to write.
75    first_gn_output: Path of first entry in action's outputs.
76    inputs: List of inputs to add to depfile.
77  """
78  assert depfile_path != first_gn_output  # http://crbug.com/646165
79  assert not isinstance(inputs, str)  # Easy mistake to make
80
81  def _process_path(path):
82    assert not os.path.isabs(path), f'Found abs path in depfile: {path}'
83    if os.path.sep != posixpath.sep:
84      path = str(pathlib.Path(path).as_posix())
85    assert '\\' not in path, f'Found \\ in depfile: {path}'
86    return path.replace(' ', '\\ ')
87
88  sb = []
89  sb.append(_process_path(first_gn_output))
90  if inputs:
91    # Sort and uniquify to ensure file is hermetic.
92    # One path per line to keep it human readable.
93    sb.append(': \\\n ')
94    sb.append(' \\\n '.join(sorted(_process_path(p) for p in set(inputs))))
95  else:
96    sb.append(': ')
97  sb.append('\n')
98
99  path = pathlib.Path(depfile_path)
100  path.parent.mkdir(parents=True, exist_ok=True)
101  path.write_text(''.join(sb))
102
103
104def parse_gn_list(value):
105  """Converts a "GN-list" command-line parameter into a list.
106
107  Conversions handled:
108    * None -> []
109    * '' -> []
110    * 'asdf' -> ['asdf']
111    * '["a", "b"]' -> ['a', 'b']
112    * ['["a", "b"]', 'c'] -> ['a', 'b', 'c']  (action='append')
113
114  This allows passing args like:
115  gn_list = [ "one", "two", "three" ]
116  args = [ "--items=$gn_list" ]
117  """
118  # Convert None to [].
119  if not value:
120    return []
121  # Convert a list of GN lists to a flattened list.
122  if isinstance(value, list):
123    ret = []
124    for arg in value:
125      ret.extend(parse_gn_list(arg))
126    return ret
127  # Convert normal GN list.
128  if value.startswith('['):
129    return gn_helpers.GNValueParser(value).ParseList()
130  # Convert a single string value to a list.
131  return [value]
132