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."""
16 from typing import Optional
17 from typing import Sequence
20 @contextlib.contextmanager
21 def atomic_output(path, mode='w+b', only_if_changed=True):
22 """Prevent half-written files and dirty mtimes for unchanged files.
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.
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.
34 with action_helpers.atomic_output(output_path) as tmp_file:
35 subprocess.check_call(['prog', '--output', tmp_file.name])
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),
47 # File should be closed before comparison/move.
49 if not (only_if_changed and os.path.exists(path)
50 and filecmp.cmp(f.name, path)):
51 shutil.move(f.name, path)
54 if os.path.exists(f.name):
58 def add_depfile_arg(parser):
59 if hasattr(parser, 'add_option'):
60 func = parser.add_option
62 func = parser.add_argument
63 func('--depfile', help='Path to depfile (refer to "gn help depfile")')
66 def write_depfile(depfile_path: str,
68 inputs: Optional[Sequence[str]] = None) -> None:
69 """Writes a ninja depfile.
71 See notes about how to use depfiles in //build/docs/writing_gn_templates.md.
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.
78 assert depfile_path != first_gn_output # http://crbug.com/646165
79 assert not isinstance(inputs, str) # Easy mistake to make
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(' ', '\\ ')
89 sb.append(_process_path(first_gn_output))
91 # Sort and uniquify to ensure file is hermetic.
92 # One path per line to keep it human readable.
94 sb.append(' \\\n '.join(sorted(_process_path(p) for p in set(inputs))))
99 path = pathlib.Path(depfile_path)
100 path.parent.mkdir(parents=True, exist_ok=True)
101 path.write_text(''.join(sb))
104 def parse_gn_list(value):
105 """Converts a "GN-list" command-line parameter into a list.
111 * '["a", "b"]' -> ['a', 'b']
112 * ['["a", "b"]', 'c'] -> ['a', 'b', 'c'] (action='append')
114 This allows passing args like:
115 gn_list = [ "one", "two", "three" ]
116 args = [ "--items=$gn_list" ]
118 # Convert None to [].
121 # Convert a list of GN lists to a flattened list.
122 if isinstance(value, list):
125 ret.extend(parse_gn_list(arg))
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.