330 lines
11 KiB
Python
Executable File
330 lines
11 KiB
Python
Executable File
#!/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
|