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 6import contextlib 7import filecmp 8import os 9import pathlib 10import posixpath 11import shutil 12import tempfile 13 14import gn_helpers 15 16from typing import Optional 17from typing import Sequence 18 19 20@contextlib.contextmanager 21def atomic_output(path, mode='w+b', only_if_changed=True): 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). 27 only_if_changed: Whether to maintain the mtime if the file has not changed. 28 Returns: 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. 32 33 Example: 34 with action_helpers.atomic_output(output_path) as tmp_file: 35 subprocess.check_call(['prog', '--output', tmp_file.name]) 36 """ 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), 42 dir=dirname, 43 delete=False) as f: 44 try: 45 yield f 46 47 # File should be closed before comparison/move. 48 f.close() 49 if not (only_if_changed and os.path.exists(path) 50 and filecmp.cmp(f.name, path)): 51 shutil.move(f.name, path) 52 finally: 53 f.close() 54 if os.path.exists(f.name): 55 os.unlink(f.name) 56 57 58def add_depfile_arg(parser): 59 if hasattr(parser, 'add_option'): 60 func = parser.add_option 61 else: 62 func = parser.add_argument 63 func('--depfile', help='Path to depfile (refer to "gn help depfile")') 64 65 66def write_depfile(depfile_path: str, 67 first_gn_output: str, 68 inputs: Optional[Sequence[str]] = None) -> None: 69 """Writes a ninja depfile. 70 71 See notes about how to use depfiles in //build/docs/writing_gn_templates.md. 72 73 Args: 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. 77 """ 78 assert depfile_path != first_gn_output # http://crbug.com/646165 79 assert not isinstance(inputs, str) # Easy mistake to make 80 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(' ', '\\ ') 87 88 sb = [] 89 sb.append(_process_path(first_gn_output)) 90 if inputs: 91 # Sort and uniquify to ensure file is hermetic. 92 # One path per line to keep it human readable. 93 sb.append(': \\\n ') 94 sb.append(' \\\n '.join(sorted(_process_path(p) for p in set(inputs)))) 95 else: 96 sb.append(': ') 97 sb.append('\n') 98 99 path = pathlib.Path(depfile_path) 100 path.parent.mkdir(parents=True, exist_ok=True) 101 path.write_text(''.join(sb)) 102 103 104def parse_gn_list(value): 105 """Converts a "GN-list" command-line parameter into a list. 106 107 Conversions handled: 108 * None -> [] 109 * '' -> [] 110 * 'asdf' -> ['asdf'] 111 * '["a", "b"]' -> ['a', 'b'] 112 * ['["a", "b"]', 'c'] -> ['a', 'b', 'c'] (action='append') 113 114 This allows passing args like: 115 gn_list = [ "one", "two", "three" ] 116 args = [ "--items=$gn_list" ] 117 """ 118 # Convert None to []. 119 if not value: 120 return [] 121 # Convert a list of GN lists to a flattened list. 122 if isinstance(value, list): 123 ret = [] 124 for arg in value: 125 ret.extend(parse_gn_list(arg)) 126 return ret 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. 131 return [value] 132