Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 824608c3 authored by Joe Onorato's avatar Joe Onorato
Browse files

Implement multitree lunch

Test: (cd build/make/orchestrator/core ; ./test_lunch.py)
Change-Id: I4ba36a79abd13c42b986e3ba0d6d599c1cc73cb0
parent d3a99576
Loading
Loading
Loading
Loading
+55 −0
Original line number Diff line number Diff line
@@ -425,6 +425,61 @@ function addcompletions()
    complete -F _complete_android_module_names m
}

function multitree_lunch_help()
{
    echo "usage: lunch PRODUCT-VARIANT" 1>&2
    echo "    Set up android build environment based on a product short name and variant" 1>&2
    echo 1>&2
    echo "lunch COMBO_FILE VARIANT" 1>&2
    echo "    Set up android build environment based on a specific lunch combo file" 1>&2
    echo "    and variant." 1>&2
    echo 1>&2
    echo "lunch --print [CONFIG]" 1>&2
    echo "    Print the contents of a configuration.  If CONFIG is supplied, that config" 1>&2
    echo "    will be flattened and printed.  If CONFIG is not supplied, the currently" 1>&2
    echo "    selected config will be printed.  Returns 0 on success or nonzero on error." 1>&2
    echo 1>&2
    echo "lunch --list" 1>&2
    echo "    List all possible combo files available in the current tree" 1>&2
    echo 1>&2
    echo "lunch --help" 1>&2
    echo "lunch -h" 1>&2
    echo "    Prints this message." 1>&2
}

function multitree_lunch()
{
    local code
    local results
    if $(echo "$1" | grep -q '^-') ; then
        # Calls starting with a -- argument are passed directly and the function
        # returns with the lunch.py exit code.
        build/make/orchestrator/core/lunch.py "$@"
        code=$?
        if [[ $code -eq 2 ]] ; then
          echo 1>&2
          multitree_lunch_help
          return $code
        elif [[ $code -ne 0 ]] ; then
          return $code
        fi
    else
        # All other calls go through the --lunch variant of lunch.py
        results=($(build/make/orchestrator/core/lunch.py --lunch "$@"))
        code=$?
        if [[ $code -eq 2 ]] ; then
          echo 1>&2
          multitree_lunch_help
          return $code
        elif [[ $code -ne 0 ]] ; then
          return $code
        fi

        export TARGET_BUILD_COMBO=${results[0]}
        export TARGET_BUILD_VARIANT=${results[1]}
    fi
}

function choosetype()
{
    echo "Build type choices are:"
+329 −0
Original line number Diff line number Diff line
#!/usr/bin/python3
#
# Copyright (C) 2022 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import argparse
import glob
import json
import os
import sys

EXIT_STATUS_OK = 0
EXIT_STATUS_ERROR = 1
EXIT_STATUS_NEED_HELP = 2

def FindDirs(path, name, ttl=6):
    """Search at most ttl directories deep inside path for a directory called name."""
    # The dance with subdirs is so that we recurse in sorted order.
    subdirs = []
    with os.scandir(path) as it:
        for dirent in sorted(it, key=lambda x: x.name):
            try:
                if dirent.is_dir():
                    if dirent.name == name:
                        yield os.path.join(path, dirent.name)
                    elif ttl > 0:
                        subdirs.append(dirent.name)
            except OSError:
                # Consume filesystem errors, e.g. too many links, permission etc.
                pass
    for subdir in subdirs:
        yield from FindDirs(os.path.join(path, subdir), name, ttl-1)


def WalkPaths(path, matcher, ttl=10):
    """Do a traversal of all files under path yielding each file that matches
    matcher."""
    # First look for files, then recurse into directories as needed.
    # The dance with subdirs is so that we recurse in sorted order.
    subdirs = []
    with os.scandir(path) as it:
        for dirent in sorted(it, key=lambda x: x.name):
            try:
                if dirent.is_file():
                    if matcher(dirent.name):
                        yield os.path.join(path, dirent.name)
                if dirent.is_dir():
                    if ttl > 0:
                        subdirs.append(dirent.name)
            except OSError:
                # Consume filesystem errors, e.g. too many links, permission etc.
                pass
    for subdir in sorted(subdirs):
        yield from WalkPaths(os.path.join(path, subdir), matcher, ttl-1)


def FindFile(path, filename):
    """Return a file called filename inside path, no more than ttl levels deep.

    Directories are searched alphabetically.
    """
    for f in WalkPaths(path, lambda x: x == filename):
        return f


def FindConfigDirs(workspace_root):
    """Find the configuration files in the well known locations inside workspace_root

        <workspace_root>/build/orchestrator/multitree_combos
           (AOSP devices, such as cuttlefish)

        <workspace_root>/vendor/**/multitree_combos
            (specific to a vendor and not open sourced)

        <workspace_root>/device/**/multitree_combos
            (specific to a vendor and are open sourced)

    Directories are returned specifically in this order, so that aosp can't be
    overridden, but vendor overrides device.
    """

    # TODO: When orchestrator is in its own git project remove the "make/" here
    yield os.path.join(workspace_root, "build/make/orchestrator/multitree_combos")

    dirs = ["vendor", "device"]
    for d in dirs:
        yield from FindDirs(os.path.join(workspace_root, d), "multitree_combos")


def FindNamedConfig(workspace_root, shortname):
    """Find the config with the given shortname inside workspace_root.

    Config directories are searched in the order described in FindConfigDirs,
    and inside those directories, alphabetically."""
    filename = shortname + ".mcombo"
    for config_dir in FindConfigDirs(workspace_root):
        found = FindFile(config_dir, filename)
        if found:
            return found
    return None


def ParseProductVariant(s):
    """Split a PRODUCT-VARIANT name, or return None if it doesn't match that pattern."""
    split = s.split("-")
    if len(split) != 2:
        return None
    return split


def ChooseConfigFromArgs(workspace_root, args):
    """Return the config file we should use for the given argument,
    or null if there's no file that matches that."""
    if len(args) == 1:
        # Prefer PRODUCT-VARIANT syntax so if there happens to be a matching
        # file we don't match that.
        pv = ParseProductVariant(args[0])
        if pv:
            config = FindNamedConfig(workspace_root, pv[0])
            if config:
                return (config, pv[1])
            return None, None
    # Look for a specifically named file
    if os.path.isfile(args[0]):
        return (args[0], args[1] if len(args) > 1 else None)
    # That file didn't exist, return that we didn't find it.
    return None, None


class ConfigException(Exception):
    ERROR_PARSE = "parse"
    ERROR_CYCLE = "cycle"

    def __init__(self, kind, message, locations, line=0):
        """Error thrown when loading and parsing configurations.

        Args:
            message: Error message to display to user
            locations: List of filenames of the include history.  The 0 index one
                       the location where the actual error occurred
        """
        if len(locations):
            s = locations[0]
            if line:
                s += ":"
                s += str(line)
            s += ": "
        else:
            s = ""
        s += message
        if len(locations):
            for loc in locations[1:]:
                s += "\n        included from %s" % loc
        super().__init__(s)
        self.kind = kind
        self.message = message
        self.locations = locations
        self.line = line


def LoadConfig(filename):
    """Load a config, including processing the inherits fields.

    Raises:
        ConfigException on errors
    """
    def LoadAndMerge(fn, visited):
        with open(fn) as f:
            try:
                contents = json.load(f)
            except json.decoder.JSONDecodeError as ex:
                if True:
                    raise ConfigException(ConfigException.ERROR_PARSE, ex.msg, visited, ex.lineno)
                else:
                    sys.stderr.write("exception %s" % ex.__dict__)
                    raise ex
            # Merge all the parents into one data, with first-wins policy
            inherited_data = {}
            for parent in contents.get("inherits", []):
                if parent in visited:
                    raise ConfigException(ConfigException.ERROR_CYCLE, "Cycle detected in inherits",
                            visited)
                DeepMerge(inherited_data, LoadAndMerge(parent, [parent,] + visited))
            # Then merge inherited_data into contents, but what's already there will win.
            DeepMerge(contents, inherited_data)
            contents.pop("inherits", None)
        return contents
    return LoadAndMerge(filename, [filename,])


def DeepMerge(merged, addition):
    """Merge all fields of addition into merged. Pre-existing fields win."""
    for k, v in addition.items():
        if k in merged:
            if isinstance(v, dict) and isinstance(merged[k], dict):
                DeepMerge(merged[k], v)
        else:
            merged[k] = v


def Lunch(args):
    """Handle the lunch command."""
    # Check that we're at the top of a multitree workspace
    # TODO: Choose the right sentinel file
    if not os.path.exists("build/make/orchestrator"):
        sys.stderr.write("ERROR: lunch.py must be run from the root of a multi-tree workspace\n")
        return EXIT_STATUS_ERROR

    # Choose the config file
    config_file, variant = ChooseConfigFromArgs(".", args)

    if config_file == None:
        sys.stderr.write("Can't find lunch combo file for: %s\n" % " ".join(args))
        return EXIT_STATUS_NEED_HELP
    if variant == None:
        sys.stderr.write("Can't find variant for: %s\n" % " ".join(args))
        return EXIT_STATUS_NEED_HELP

    # Parse the config file
    try:
        config = LoadConfig(config_file)
    except ConfigException as ex:
        sys.stderr.write(str(ex))
        return EXIT_STATUS_ERROR

    # Fail if the lunchable bit isn't set, because this isn't a usable config
    if not config.get("lunchable", False):
        sys.stderr.write("%s: Lunch config file (or inherited files) does not have the 'lunchable'"
                % config_file)
        sys.stderr.write(" flag set, which means it is probably not a complete lunch spec.\n")

    # All the validation has passed, so print the name of the file and the variant
    sys.stdout.write("%s\n" % config_file)
    sys.stdout.write("%s\n" % variant)

    return EXIT_STATUS_OK


def FindAllComboFiles(workspace_root):
    """Find all .mcombo files in the prescribed locations in the tree."""
    for dir in FindConfigDirs(workspace_root):
        for file in WalkPaths(dir, lambda x: x.endswith(".mcombo")):
            yield file


def IsFileLunchable(config_file):
    """Parse config_file, flatten the inheritance, and return whether it can be
    used as a lunch target."""
    try:
        config = LoadConfig(config_file)
    except ConfigException as ex:
        sys.stderr.write("%s" % ex)
        return False
    return config.get("lunchable", False)


def FindAllLunchable(workspace_root):
    """Find all mcombo files in the tree (rooted at workspace_root) that when
    parsed (and inheritance is flattened) have lunchable: true."""
    for f in [x for x in FindAllComboFiles(workspace_root) if IsFileLunchable(x)]:
        yield f


def List():
    """Handle the --list command."""
    for f in sorted(FindAllLunchable(".")):
        print(f)


def Print(args):
    """Handle the --print command."""
    # Parse args
    if len(args) == 0:
        config_file = os.environ.get("TARGET_BUILD_COMBO")
        if not config_file:
            sys.stderr.write("TARGET_BUILD_COMBO not set. Run lunch or pass a combo file.\n")
            return EXIT_STATUS_NEED_HELP
    elif len(args) == 1:
        config_file = args[0]
    else:
        return EXIT_STATUS_NEED_HELP

    # Parse the config file
    try:
        config = LoadConfig(config_file)
    except ConfigException as ex:
        sys.stderr.write(str(ex))
        return EXIT_STATUS_ERROR

    # Print the config in json form
    json.dump(config, sys.stdout, indent=4)

    return EXIT_STATUS_OK


def main(argv):
    if len(argv) < 2 or argv[1] == "-h" or argv[1] == "--help":
        return EXIT_STATUS_NEED_HELP

    if len(argv) == 2 and argv[1] == "--list":
        List()
        return EXIT_STATUS_OK

    if len(argv) == 2 and argv[1] == "--print":
        return Print(argv[2:])
        return EXIT_STATUS_OK

    if (len(argv) == 2 or len(argv) == 3) and argv[1] == "--lunch":
        return Lunch(argv[2:])

    sys.stderr.write("Unknown lunch command: %s\n" % " ".join(argv[1:]))
    return EXIT_STATUS_NEED_HELP

if __name__ == "__main__":
    sys.exit(main(sys.argv))


# vim: sts=4:ts=4:sw=4
+1 −0
Original line number Diff line number Diff line
{}
+1 −0
Original line number Diff line number Diff line
a
+1 −0
Original line number Diff line number Diff line
INVALID FILE
Loading