Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 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 | |
| 6 | import contextlib |
| 7 | import filecmp |
| 8 | import os |
| 9 | import pathlib |
Andrew Grieve | 4a10220 | 2023-04-05 02:03:27 | [diff] [blame] | 10 | import posixpath |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 11 | import shutil |
| 12 | import tempfile |
| 13 | |
| 14 | import gn_helpers |
| 15 | |
Daniel Cheng | 3379b1be | 2023-09-05 17:24:49 | [diff] [blame] | 16 | from typing import Optional |
| 17 | from typing import Sequence |
| 18 | |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 19 | |
| 20 | @contextlib.contextmanager |
Andrew Grieve | 74ce100 | 2025-06-23 15:29:14 | [diff] [blame] | 21 | def atomic_output(path, mode='w+b', encoding=None, only_if_changed=True): |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 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). |
Andrew Grieve | 74ce100 | 2025-06-23 15:29:14 | [diff] [blame] | 27 | encoding: Encoding to use if using non-binary mode. |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 28 | 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 Grieve | 74ce100 | 2025-06-23 15:29:14 | [diff] [blame] | 41 | if encoding is not None and mode == 'w+b': |
| 42 | mode = 'w+' |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 43 | with tempfile.NamedTemporaryFile(mode, |
Andrew Grieve | 74ce100 | 2025-06-23 15:29:14 | [diff] [blame] | 44 | encoding=encoding, |
Fumitoshi Ukai | 269f5cb | 2025-05-13 06:59:37 | [diff] [blame] | 45 | prefix=".tempfile.", |
| 46 | suffix="." + os.path.basename(path), |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 47 | dir=dirname, |
| 48 | delete=False) as f: |
| 49 | try: |
| 50 | yield f |
| 51 | |
Andrew Grieve | 4a10220 | 2023-04-05 02:03:27 | [diff] [blame] | 52 | # File should be closed before comparison/move. |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 53 | 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 Grieve | 4a10220 | 2023-04-05 02:03:27 | [diff] [blame] | 58 | f.close() |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 59 | if os.path.exists(f.name): |
| 60 | os.unlink(f.name) |
| 61 | |
| 62 | |
| 63 | def 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 Cheng | 3379b1be | 2023-09-05 17:24:49 | [diff] [blame] | 71 | def write_depfile(depfile_path: str, |
| 72 | first_gn_output: str, |
| 73 | inputs: Optional[Sequence[str]] = None) -> None: |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 74 | """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 Grieve | 4a10220 | 2023-04-05 02:03:27 | [diff] [blame] | 86 | 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 Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 92 | |
| 93 | sb = [] |
Andrew Grieve | 4a10220 | 2023-04-05 02:03:27 | [diff] [blame] | 94 | sb.append(_process_path(first_gn_output)) |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 95 | 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 Grieve | 4a10220 | 2023-04-05 02:03:27 | [diff] [blame] | 99 | sb.append(' \\\n '.join(sorted(_process_path(p) for p in set(inputs)))) |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 100 | 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 Ukai | a8a65e5f | 2025-07-01 05:23:01 | [diff] [blame] | 106 | with atomic_output(str(path), mode='w', encoding='utf-8') as w: |
| 107 | w.write(''.join(sb)) |
Andrew Grieve | 9d2a1ed | 2023-03-30 19:08:38 | [diff] [blame] | 108 | |
| 109 | |
| 110 | def 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] |