tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

action_helpers.py (4037B)


      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
     10 import posixpath
     11 import shutil
     12 import tempfile
     13 
     14 import gn_helpers
     15 
     16 from typing import Optional
     17 from typing import Sequence
     18 
     19 
     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.
     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 
     58 def 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 
     66 def 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 
    104 def 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]