blob: 7399716cbb2d7cb3e28bd16aa3b7784609de9db4 [file] [log] [blame]
Andrew Grieve9d2a1ed2023-03-30 19:08:381# 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
Andrew Grieve4a102202023-04-05 02:03:2710import posixpath
Andrew Grieve9d2a1ed2023-03-30 19:08:3811import shutil
12import tempfile
13
14import gn_helpers
15
Daniel Cheng3379b1be2023-09-05 17:24:4916from typing import Optional
17from typing import Sequence
18
Andrew Grieve9d2a1ed2023-03-30 19:08:3819
20@contextlib.contextmanager
Andrew Grieve74ce1002025-06-23 15:29:1421def atomic_output(path, mode='w+b', encoding=None, only_if_changed=True):
Andrew Grieve9d2a1ed2023-03-30 19:08:3822 """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).
Andrew Grieve74ce1002025-06-23 15:29:1427 encoding: Encoding to use if using non-binary mode.
Andrew Grieve9d2a1ed2023-03-30 19:08:3828 only_if_changed: Whether to maintain the mtime if the file has not changed.
29 Returns:
30 A Context Manager that yields a NamedTemporaryFile instance. On exit, the
31 manager will check if the file contents is different from the destination
32 and if so, move it into place.
33
34 Example:
35 with action_helpers.atomic_output(output_path) as tmp_file:
36 subprocess.check_call(['prog', '--output', tmp_file.name])
37 """
38 # Create in same directory to ensure same filesystem when moving.
39 dirname = os.path.dirname(path) or '.'
40 os.makedirs(dirname, exist_ok=True)
Andrew Grieve74ce1002025-06-23 15:29:1441 if encoding is not None and mode == 'w+b':
42 mode = 'w+'
Andrew Grieve9d2a1ed2023-03-30 19:08:3843 with tempfile.NamedTemporaryFile(mode,
Andrew Grieve74ce1002025-06-23 15:29:1444 encoding=encoding,
Fumitoshi Ukai269f5cb2025-05-13 06:59:3745 prefix=".tempfile.",
46 suffix="." + os.path.basename(path),
Andrew Grieve9d2a1ed2023-03-30 19:08:3847 dir=dirname,
48 delete=False) as f:
49 try:
50 yield f
51
Andrew Grieve4a102202023-04-05 02:03:2752 # File should be closed before comparison/move.
Andrew Grieve9d2a1ed2023-03-30 19:08:3853 f.close()
54 if not (only_if_changed and os.path.exists(path)
55 and filecmp.cmp(f.name, path)):
56 shutil.move(f.name, path)
57 finally:
Andrew Grieve4a102202023-04-05 02:03:2758 f.close()
Andrew Grieve9d2a1ed2023-03-30 19:08:3859 if os.path.exists(f.name):
60 os.unlink(f.name)
61
62
63def add_depfile_arg(parser):
64 if hasattr(parser, 'add_option'):
65 func = parser.add_option
66 else:
67 func = parser.add_argument
68 func('--depfile', help='Path to depfile (refer to "gn help depfile")')
69
70
Daniel Cheng3379b1be2023-09-05 17:24:4971def write_depfile(depfile_path: str,
72 first_gn_output: str,
73 inputs: Optional[Sequence[str]] = None) -> None:
Andrew Grieve9d2a1ed2023-03-30 19:08:3874 """Writes a ninja depfile.
75
76 See notes about how to use depfiles in //build/docs/writing_gn_templates.md.
77
78 Args:
79 depfile_path: Path to file to write.
80 first_gn_output: Path of first entry in action's outputs.
81 inputs: List of inputs to add to depfile.
82 """
83 assert depfile_path != first_gn_output # http://crbug.com/646165
84 assert not isinstance(inputs, str) # Easy mistake to make
85
Andrew Grieve4a102202023-04-05 02:03:2786 def _process_path(path):
87 assert not os.path.isabs(path), f'Found abs path in depfile: {path}'
88 if os.path.sep != posixpath.sep:
89 path = str(pathlib.Path(path).as_posix())
90 assert '\\' not in path, f'Found \\ in depfile: {path}'
91 return path.replace(' ', '\\ ')
Andrew Grieve9d2a1ed2023-03-30 19:08:3892
93 sb = []
Andrew Grieve4a102202023-04-05 02:03:2794 sb.append(_process_path(first_gn_output))
Andrew Grieve9d2a1ed2023-03-30 19:08:3895 if inputs:
96 # Sort and uniquify to ensure file is hermetic.
97 # One path per line to keep it human readable.
98 sb.append(': \\\n ')
Andrew Grieve4a102202023-04-05 02:03:2799 sb.append(' \\\n '.join(sorted(_process_path(p) for p in set(inputs))))
Andrew Grieve9d2a1ed2023-03-30 19:08:38100 else:
101 sb.append(': ')
102 sb.append('\n')
103
104 path = pathlib.Path(depfile_path)
105 path.parent.mkdir(parents=True, exist_ok=True)
Fumitoshi Ukaia8a65e5f2025-07-01 05:23:01106 with atomic_output(str(path), mode='w', encoding='utf-8') as w:
107 w.write(''.join(sb))
Andrew Grieve9d2a1ed2023-03-30 19:08:38108
109
110def parse_gn_list(value):
111 """Converts a "GN-list" command-line parameter into a list.
112
113 Conversions handled:
114 * None -> []
115 * '' -> []
116 * 'asdf' -> ['asdf']
117 * '["a", "b"]' -> ['a', 'b']
118 * ['["a", "b"]', 'c'] -> ['a', 'b', 'c'] (action='append')
119
120 This allows passing args like:
121 gn_list = [ "one", "two", "three" ]
122 args = [ "--items=$gn_list" ]
123 """
124 # Convert None to [].
125 if not value:
126 return []
127 # Convert a list of GN lists to a flattened list.
128 if isinstance(value, list):
129 ret = []
130 for arg in value:
131 ret.extend(parse_gn_list(arg))
132 return ret
133 # Convert normal GN list.
134 if value.startswith('['):
135 return gn_helpers.GNValueParser(value).ParseList()
136 # Convert a single string value to a list.
137 return [value]