chromium/tools/flags/generate_unexpire_flags.py

#!/usr/bin/env python3
# Copyright 2020 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generates extra flags needed to allow temporarily reverting flag expiry.

This program generates three files:
* A C++ source file, containing definitions of base::Features that unexpire
  flags that expired in recent milestones, along with a definition of a
  definition of a function `flags::ExpiryEnabledForMilestone`
* A C++ header file, containing declarations of those base::Features
* A C++ source fragment, containing definitions of flags_ui::FeatureEntry
  structures for flags corresponding to those base::Features

Which milestones are recent is sourced from //chrome/VERSION in the source tree.
"""

import os
import sys

ROOT_PATH = os.path.join(os.path.dirname(__file__), '..', '..')


def get_chromium_version():
  """Parses the chromium version out of //chrome/VERSION."""
  with open(os.path.join(ROOT_PATH, 'chrome', 'VERSION')) as f:
    for line in f.readlines():
      key, value = line.strip().split('=')
      if key == 'MAJOR':
        return int(value)
  return None


def recent_mstones(mstone):
  """Returns the list of milestones considered 'recent' for the given mstone.

  Flag unexpiry is available only for flags that expired at recent mstones."""
  return [mstone - 1, mstone]


def file_header(prog_name):
  """Returns the header to use on generated files."""
  return """// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// This is a generated file. Do not edit it! It was generated by:
//   {prog_name}
""".format(prog_name=prog_name)


def gen_features_impl(prog_name, mstone):
  """Generates the definitions for the unexpiry features and the expiry-check
     function.

  This function generates the contents of a complete C++ source file,
  which defines base::Features for unexpiration of flags from recent milestones,
  as well as a function ExpiryEnabledForMilestone().
  """
  body = file_header(prog_name)
  body += """
#include "base/feature_list.h"
#include "chrome/browser/unexpire_flags_gen.h"

namespace flags {

"""

  features = [(m, 'UnexpireFlagsM' + str(m)) for m in recent_mstones(mstone)]
  for feature in features:
    body += f'BASE_FEATURE(k{feature[1]},\n'
    body += f'             "{feature[1]}",\n'
    body += f'             base::FEATURE_DISABLED_BY_DEFAULT);\n\n'

  body += """// Returns the unexpire feature for the given mstone, if any.
const base::Feature* GetUnexpireFeatureForMilestone(int milestone) {
  switch (milestone) {
"""

  for feature in features:
    body += '    case {m}: return &k{f};\n'.format(m=feature[0], f=feature[1])
  body += """    default: return nullptr;
  }
}

}  // namespace flags
"""

  return body


def gen_features_header(prog_name, mstone):
  """Generate a header file declaring features and the expiry predicate.

  This header declares the features and function described in
  gen_features_impl().
  """
  body = file_header(prog_name)

  body += """
#ifndef GEN_CHROME_BROWSER_UNEXPIRE_FLAGS_GEN_H_
#define GEN_CHROME_BROWSER_UNEXPIRE_FLAGS_GEN_H_

namespace flags {

"""

  for m in recent_mstones(mstone):
    body += f'BASE_DECLARE_FEATURE(kUnexpireFlagsM{m});\n'

  body += """
// Returns the base::Feature used to decide whether flag expiration is enabled
// for a given milestone, if there is such a feature. If not, returns nullptr.
const base::Feature* GetUnexpireFeatureForMilestone(int milestone);

}  // namespace flags

#endif  // GEN_CHROME_BROWSER_UNEXPIRE_FLAGS_GEN_H_
"""

  return body


def gen_flags_fragment(prog_name, mstone):
  """Generates a .inc file containing flag definitions.

  This creates a C++ source fragment defining flags, which are bound to the
  features described in gen_features_impl().
  """
  # Note: The exact format of the flag name (temporary-unexpire-flags-m{m}) is
  # depended on by a hack in UnexpiredMilestonesFromStorage(). See
  # https://crbug.com/1101828 for more details.
  fragment = """
    {{"temporary-unexpire-flags-m{m}",
     "Temporarily unexpire M{m} flags.",
     "Temporarily unexpire flags that expired as of M{m}. These flags will be"
     " removed soon.",
     kOsAll | flags_ui::kFlagInfrastructure,
     FEATURE_VALUE_TYPE(flags::kUnexpireFlagsM{m})}},
"""

  return '\n'.join([fragment.format(m=m) for m in recent_mstones(mstone)])


def update_file_if_stale(filename, data):
  """Writes data to filename if data is different from file's contents on disk.
  """
  try:
    disk_data = open(filename, 'r').read()
    if disk_data == data:
      return
  except IOError:
    pass
  open(filename, 'w').write(data)


def main():
  mstone = get_chromium_version()

  if not mstone:
    raise ValueError('Can\'t find or understand //chrome/VERSION')

  progname = sys.argv[0]

  # Note the mstone - 1 here: the listed expiration mstone is the last mstone in
  # which that flag is present, not the first mstone in which it is not present.
  update_file_if_stale(sys.argv[1], gen_features_impl(progname, mstone - 1))
  update_file_if_stale(sys.argv[2], gen_features_header(progname, mstone - 1))
  update_file_if_stale(sys.argv[3], gen_flags_fragment(progname, mstone - 1))


if __name__ == '__main__':
  main()