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