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