chromium/media/ffmpeg/scripts/robo_lib/config.py

# 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.

""" Contains the global configuration object.
"""

import os
import platform
import re
from . import shell
from . import packages
from . import errors


class RoboConfiguration:
    __slots__ = ('_sushi_branch_prefix', '_gn_commit_title',
                 '_patches_commit_title', '_readme_chromium_commit_title',
                 '_origin_merge_base', '_llvm_path', '_autorename_git_file',
                 '_chrome_src', '_host_operating_system', '_host_architecture',
                 '_ffmpeg_home', '_relative_asan_directory', '_branch_name',
                 '_sushi_branch_name', '_readme_chromium_commit_title',
                 '_nasm_path', '_prompt_on_call', '_os_flavor',
                 '_force_gn_rebuild', '_skip_allowed', '_script_directory',
                 '_relative_x86_directory')

    def __init__(self, quiet=False):
        # Important: Robosushi might be running the --setup command, so some of
        # the sanity checks should only be for things that we don't plan to fix
        # as part of that.

        self.set_prompt_on_call(False)
        # Pull this from parsed args
        self._force_gn_rebuild = False
        # Allowed to skip steps, default to yes, needs --no-skip flag to disable.
        self._skip_allowed = True
        # This is the prefix that our branches start with.
        self._sushi_branch_prefix = "sushi-"
        # This is the title that we use for the commit with GN configs.
        self._gn_commit_title = "GN Configuration"
        # Title of the commit with chromium/patches/README.
        self._patches_commit_title = "Chromium patches file"
        # Title of the commit with README.chromium
        self._readme_chromium_commit_title = "README.chromium file"

        self.EnsureHostInfo()
        self.EnsureChromeSrc()
        self.EnsureScriptDirectory()

        # Origin side of the merge. Only needs to change if you're trying to
        # modify and test robosushi itself. See robosushi.py for details.
        self._origin_merge_base = "origin/master"  # nocheck

        # Directory where llvm lives.
        self._llvm_path = os.path.join(self.chrome_src(), "third_party",
                                       "llvm-build", "Release+Asserts", "bin")

        self.EnsurePathContainsLLVM()
        self.EnsureNoMakeInfo()
        self.EnsureFFmpegHome()
        self.EnsureGNConfig()
        self.ComputeBranchName()

        if not quiet:
            shell.log(f"Using chrome src: {self.chrome_src()}")
            shell.log(f"Using script dir: {self._script_directory}")
            shell.log(f"Using ffmpeg home: {self.ffmpeg_home()}")
            shell.log(f"On branch: {self.branch_name()}")
            if self.sushi_branch_name():
                shell.log(f"On sushi branch: {self.sushi_branch_name()}")

        # Filename that we'll ask generate_gn.py to write git commands to.
        # TODO: Should this use script_directory, or stay with ffmpeg?  As long as
        # there's a .gitignore entry, either should be fine.
        self._autorename_git_file = os.path.join(self.ffmpeg_home(),
                                                 "scripts",
                                                 ".git_commands.sh")

    def chrome_src(self):
        return self._chrome_src

    def chdir_to_chrome_src(self):
        os.chdir(self.chrome_src())

    def scripts_dir(self):
        return self._script_directory

    def get_script_path(self, *parts):
        return os.path.join(self.scripts_dir(), *parts)

    def ffmpeg_home(self):
        return self._ffmpeg_home

    def chdir_to_ffmpeg_home(self):
        os.chdir(self.ffmpeg_home())

    def ffmpeg_src(self):
        # ffmpeg_src is currently the same as ffmpeg_home, but this is not
        # required to be the same place if sources were to move.
        return self.ffmpeg_home()

    def chdir_to_ffmpeg_src(self):
        os.chdir(self.ffmpeg_src())

    def target_config_directory(self, arch, opsys, target):
        return os.path.join(self.ffmpeg_home(), f'build.{arch}.{opsys}', target)

    def patches_dir_location(self):
        return os.path.join(self.ffmpeg_home(), "chromium", "patches")

    def exported_configs_directory(self, arch, opsys, target):
        return os.path.join(
            self.ffmpeg_home(), "chromium", "config", target, opsys, arch)

    def autorename_git_file(self):
        return self.get_script_path('git_commands.sh')

    def host_operating_system(self):
        """Return host type, e.g. "linux" """
        return self._host_operating_system

    def host_architecture(self):
        """Return host architecture, e.g. "x64" """
        return self._host_architecture

    def relative_asan_directory(self):
        return self._relative_asan_directory

    def absolute_asan_directory(self):
        return os.path.join(self.chrome_src(), self.relative_asan_directory())

    def relative_x86_directory(self):
        return self._relative_x86_directory

    def absolute_x86_directory(self):
        return os.path.join(self.chrome_src(), self.relative_x86_directory())

    def branch_name(self):
        """Return the current workspace's branch name, or None."""
        return self._branch_name

    def sushi_branch_name(self):
        """If the workspace is currently on a branch that we created (a "sushi
    branch"), return it.  Else return None."""
        return self._sushi_branch_name

    def sushi_branch_prefix(self):
        """Return the branch name that indicates that this is a "sushi branch"."""
        return self._sushi_branch_prefix

    def gn_commit_title(self):
        return self._gn_commit_title

    def patches_commit_title(self):
        return self._patches_commit_title

    def readme_chromium_commit_title(self):
        return self._readme_chromium_commit_title

    def nasm_path(self):
        return self._nasm_path

    def origin_merge_base(self):
        return self._origin_merge_base

    def override_origin_merge_base(self, new_base):
        self._origin_merge_base = new_base

    def os_flavor(self):
        return self._os_flavor

    def force_gn_rebuild(self):
        return self._force_gn_rebuild

    def set_force_gn_rebuild(self):
        self._force_gn_rebuild = True

    def skip_allowed(self):
        return self._skip_allowed

    def set_skip_allowed(self, to):
        self._skip_allowed = to

    def EnsureHostInfo(self):
        """Ensure that the host architecture and platform are set."""

        if re.match(r"i.86", platform.machine()):
            self._host_architecture = "ia32"
        elif platform.machine() == "x86_64" or platform.machine() == "AMD64":
            self._host_architecture = "x64"
        elif platform.machine() == "aarch64":
            self._host_architecture = "arm64"
        elif platform.machine() == "mips32":
            self._host_architecture = "mipsel"
        elif platform.machine() == "mips64":
            self._host_architecture = "mips64el"
        elif platform.machine().startswith("arm"):
            self._host_architecture = "arm"
        else:
            raise ValueError(
                f"Unrecognized CPU architecture: {platform.machine()}")

        if platform.system() == "Linux":
            self._host_operating_system = "linux"

            try:
                with open("/etc/lsb-release", "r") as f:
                    result = f.read()
                    if "Ubuntu" in result or "Debian" in result:
                        self._os_flavor = packages.OsFlavor.Debian
                    elif "Arch" in result:
                        self._os_flavor = packages.OsFlavor.Arch
                    else:
                        raise Exception(
                            "Couldn't determine OS flavor from lsb-release "
                            "(needed to install packages)")
            except:
                raise Exception(
                    "Couldn't read OS flavor from /etc/lsb-release file "
                    "(needed to install packages)")
        elif platform.system() == "Darwin":
            self._host_operating_system = "mac"
        elif platform.system() == "Windows" or "CYGWIN_NT" in platform.system(
        ):
            self._host_operating_system = "win"
        else:
            raise ValueError(f"Unsupported platform: {platform.system()}")

    def EnsureChromeSrc(self):
        """Find the /absolute/path/to/my_chrome_dir/src"""
        wd = os.getcwd()
        # Walk up the tree until we find src/AUTHORS
        while wd != "/":
            if os.path.isfile(os.path.join(wd, "src", "AUTHORS")):
                self._chrome_src = os.path.join(wd, "src")
                return
            wd = os.path.dirname(wd)
        raise Exception("could not find src/AUTHORS in any parent of the wd")

    def EnsureScriptDirectory(self):
        """Make sure we know where the scripts are."""
        # Assume that __func__ is /.../scripts/robo_lib/something.py
        self._script_directory = os.path.dirname(os.path.dirname(__file__))
        # Verify that `robosushi.py` is in this directory, for sanity.
        if not os.path.isfile(self.get_script_path("robosushi.py")):
            raise Exception("Fix EnsureScriptDir -- cannot find robosushi.py")

    def EnsureFFmpegHome(self):
        """Ensure that |self| has "ffmpeg_home" set."""
        self._ffmpeg_home = os.path.join(self.chrome_src(), "third_party",
                                         "ffmpeg")

    def EnsureGNConfig(self):
        """Find the gn directories.  Note that we don't create them."""
        # These are suitable for 'autoninja -C'
        self._relative_asan_directory = os.path.join("out", "sushi_asan")
        self._relative_x86_directory = os.path.join("out", "sushi_x86")

    def EnsurePathContainsLLVM(self):
        """Make sure that we have chromium's LLVM in $PATH.

    We don't want folks to accidentally use the wrong clang.
    """

        llvm_path = os.path.join(self.chrome_src(), "third_party",
                                 "llvm-build", "Release+Asserts", "bin")
        if self.llvm_path() not in os.environ["PATH"]:
            raise errors.UserInstructions(
                "Please add:\n%s\nto the beginning of $PATH\nExample: export PATH=%s:$PATH"
                % (self.llvm_path(), self.llvm_path()))

    def EnsureNoMakeInfo(self):
        """Ensure that makeinfo is not available."""
        if os.system("makeinfo --version > /dev/null 2>&1") == 0:
            raise errors.UserInstructions(
                "makeinfo is available and we don't need it, so please remove it\nExample: sudo apt-get remove texinfo"
            )

    def llvm_path(self):
        return self._llvm_path

    def ComputeBranchName(self):
        """Get the current branch name and set it."""
        self.chdir_to_ffmpeg_src()
        branch_name = shell.output_or_error(
            ["git", "rev-parse", "--abbrev-ref", "HEAD"])
        self.SetBranchName(str(branch_name))

    def SetBranchName(self, name):
        """Set our branch name, which may be a sushi branch or not."""
        self._branch_name = name
        # If this is one of our branches, then record that too.
        if name and not name.startswith(self.sushi_branch_prefix()):
            name = None
        self._sushi_branch_name = name

    def prompt_on_call(self):
        """ Return True if and only if we're supposed to ask the user before running
    any command that might have a side-effect."""
        return self._prompt_on_call

    def set_prompt_on_call(self, value):
        self._prompt_on_call = value

    def Call(self, args, **kwargs):
        """Run the command specified by |args| (see subprocess.call), optionally
    prompting the user."""
        if self.prompt_on_call():
            print(f"[{os.getcwd()}] About to run: `{' '.join(args)}` ")
            input("Press ENTER to continue, or interrupt the script: ")
        return shell.check_run(args, **kwargs)