1
    2
    3
    4
    5
    6
    7
    8
    9
   10
   11
   12
   13
   14
   15
   16
   17
   18
   19
   20
   21
   22
   23
   24
   25
   26
   27
   28
   29
   30
   31
   32
   33
   34
   35
   36
   37
   38
   39
   40
   41
   42
   43
   44
   45
   46
   47
   48
   49
   50
   51
   52
   53
   54
   55
   56
   57
   58
   59
   60
   61
   62
   63
   64
   65
   66
   67
   68
   69
   70
   71
   72
   73
   74
   75
   76
   77
   78
   79
   80
   81
   82
   83
   84
   85
   86
   87
   88
   89
   90
   91
   92
   93
   94
   95
   96
   97

infra / config / migration / buildozer.py [blame]

# Copyright 2024 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Module providing utilities for running buildozer."""

import pathlib
import subprocess
import sys

_THIS_DIR = pathlib.Path(__file__).parent

# Override the buildozer tables with empty tables to avoid buildozer making
# unintended changes such as sorting most lists, including in existing portions
# of the file
_TABLES_JSON_FILE = _THIS_DIR / 'buildozer-tables.json'


def _run(*args: str) -> subprocess.CompletedProcess:
  # output is always captured to avoid having each edit trigger a repetitive
  # line of output
  return subprocess.run(
      [
          'buildozer',
          # Use empty tables
          f'-tables={_TABLES_JSON_FILE}',
          # Add comments above the element being commented instead of at the end
          # of the line
          '-eol-comments=false',
          *args,
      ],
      capture_output=True,
      encoding='utf-8',
  )


def run(*args: str) -> str:
  """Run buildozer.

  buildozer is run with the provided arguments, with the output being
  captured. In the case of a usage error (exit code 1) or command
  failure (exit code 2), the process' stderr will be written to stderr.
  Exit code 3 indicates success with no changes and isn't treated as an
  error.

  Args:
    args: The commands and targets to buildozer.

  Returns:
    The stdout of the buildozer process.

  Raises:
    subprocess.CalledProcessError if there is a usage error or command
    failure when running buildozer.
  """
  ret = _run(*args)
  # buildozer returns exit code of 3 when it makes no changes, the edits should
  # be idempotent, so we have to manually check the return code
  if ret.returncode not in (0, 3):
    sys.stderr.write(ret.stderr)
    ret.check_returncode()
  return ret.stdout


def try_run(*args: str) -> bool:
  """Run buildozer, allowing for command failure.

  buildozer is run with the provided arguments, with the output being
  captured. In the case of a usage error (exit code 1), the process'
  stderr will be written to stderr. A command failure (exit code 2) will
  be reported via a False return value, with no output be written to
  stdout or stderr. Exit code 3 indicates success with no changes and
  isn't treated as an error.

  Args:
    args: The commands and targets to buildozer.

  Returns:
    Whether or not the buildozer commands succeeded.

  Raises:
    subprocess.CalledProcessError if there is a usage error when running
    buildozer.
  """
  ret = _run(*args)
  # buildozer returns exit code 2 when a command fails (e.g the target doesn't
  # exist), as opposed to a usage error (e.g not enough arguments to a command)
  # which returns 1. The caller is expecting that the command might fail, so
  # don't raise an exception, instead just return False to indicate the failure.
  if ret.returncode == 2:
    sys.stderr.write(ret.stderr)
    return False
  # buildozer returns exit code of 3 when it makes no changes, the edits should
  # be idempotent, so we have to manually check the return code
  if ret.returncode not in (0, 3):
    sys.stderr.write(ret.stderr)
    ret.check_returncode()
  return True