Skip to content

Commit 824608c

Browse files
committed
Implement multitree lunch
Test: (cd build/make/orchestrator/core ; ./test_lunch.py) Change-Id: I4ba36a79abd13c42b986e3ba0d6d599c1cc73cb0
1 parent d3a9957 commit 824608c

File tree

23 files changed

+593
-0
lines changed

23 files changed

+593
-0
lines changed

envsetup.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,61 @@ function addcompletions()
425425
complete -F _complete_android_module_names m
426426
}
427427

428+
function multitree_lunch_help()
429+
{
430+
echo "usage: lunch PRODUCT-VARIANT" 1>&2
431+
echo " Set up android build environment based on a product short name and variant" 1>&2
432+
echo 1>&2
433+
echo "lunch COMBO_FILE VARIANT" 1>&2
434+
echo " Set up android build environment based on a specific lunch combo file" 1>&2
435+
echo " and variant." 1>&2
436+
echo 1>&2
437+
echo "lunch --print [CONFIG]" 1>&2
438+
echo " Print the contents of a configuration. If CONFIG is supplied, that config" 1>&2
439+
echo " will be flattened and printed. If CONFIG is not supplied, the currently" 1>&2
440+
echo " selected config will be printed. Returns 0 on success or nonzero on error." 1>&2
441+
echo 1>&2
442+
echo "lunch --list" 1>&2
443+
echo " List all possible combo files available in the current tree" 1>&2
444+
echo 1>&2
445+
echo "lunch --help" 1>&2
446+
echo "lunch -h" 1>&2
447+
echo " Prints this message." 1>&2
448+
}
449+
450+
function multitree_lunch()
451+
{
452+
local code
453+
local results
454+
if $(echo "$1" | grep -q '^-') ; then
455+
# Calls starting with a -- argument are passed directly and the function
456+
# returns with the lunch.py exit code.
457+
build/make/orchestrator/core/lunch.py "$@"
458+
code=$?
459+
if [[ $code -eq 2 ]] ; then
460+
echo 1>&2
461+
multitree_lunch_help
462+
return $code
463+
elif [[ $code -ne 0 ]] ; then
464+
return $code
465+
fi
466+
else
467+
# All other calls go through the --lunch variant of lunch.py
468+
results=($(build/make/orchestrator/core/lunch.py --lunch "$@"))
469+
code=$?
470+
if [[ $code -eq 2 ]] ; then
471+
echo 1>&2
472+
multitree_lunch_help
473+
return $code
474+
elif [[ $code -ne 0 ]] ; then
475+
return $code
476+
fi
477+
478+
export TARGET_BUILD_COMBO=${results[0]}
479+
export TARGET_BUILD_VARIANT=${results[1]}
480+
fi
481+
}
482+
428483
function choosetype()
429484
{
430485
echo "Build type choices are:"

orchestrator/core/lunch.py

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
#!/usr/bin/python3
2+
#
3+
# Copyright (C) 2022 The Android Open Source Project
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import argparse
18+
import glob
19+
import json
20+
import os
21+
import sys
22+
23+
EXIT_STATUS_OK = 0
24+
EXIT_STATUS_ERROR = 1
25+
EXIT_STATUS_NEED_HELP = 2
26+
27+
def FindDirs(path, name, ttl=6):
28+
"""Search at most ttl directories deep inside path for a directory called name."""
29+
# The dance with subdirs is so that we recurse in sorted order.
30+
subdirs = []
31+
with os.scandir(path) as it:
32+
for dirent in sorted(it, key=lambda x: x.name):
33+
try:
34+
if dirent.is_dir():
35+
if dirent.name == name:
36+
yield os.path.join(path, dirent.name)
37+
elif ttl > 0:
38+
subdirs.append(dirent.name)
39+
except OSError:
40+
# Consume filesystem errors, e.g. too many links, permission etc.
41+
pass
42+
for subdir in subdirs:
43+
yield from FindDirs(os.path.join(path, subdir), name, ttl-1)
44+
45+
46+
def WalkPaths(path, matcher, ttl=10):
47+
"""Do a traversal of all files under path yielding each file that matches
48+
matcher."""
49+
# First look for files, then recurse into directories as needed.
50+
# The dance with subdirs is so that we recurse in sorted order.
51+
subdirs = []
52+
with os.scandir(path) as it:
53+
for dirent in sorted(it, key=lambda x: x.name):
54+
try:
55+
if dirent.is_file():
56+
if matcher(dirent.name):
57+
yield os.path.join(path, dirent.name)
58+
if dirent.is_dir():
59+
if ttl > 0:
60+
subdirs.append(dirent.name)
61+
except OSError:
62+
# Consume filesystem errors, e.g. too many links, permission etc.
63+
pass
64+
for subdir in sorted(subdirs):
65+
yield from WalkPaths(os.path.join(path, subdir), matcher, ttl-1)
66+
67+
68+
def FindFile(path, filename):
69+
"""Return a file called filename inside path, no more than ttl levels deep.
70+
71+
Directories are searched alphabetically.
72+
"""
73+
for f in WalkPaths(path, lambda x: x == filename):
74+
return f
75+
76+
77+
def FindConfigDirs(workspace_root):
78+
"""Find the configuration files in the well known locations inside workspace_root
79+
80+
<workspace_root>/build/orchestrator/multitree_combos
81+
(AOSP devices, such as cuttlefish)
82+
83+
<workspace_root>/vendor/**/multitree_combos
84+
(specific to a vendor and not open sourced)
85+
86+
<workspace_root>/device/**/multitree_combos
87+
(specific to a vendor and are open sourced)
88+
89+
Directories are returned specifically in this order, so that aosp can't be
90+
overridden, but vendor overrides device.
91+
"""
92+
93+
# TODO: When orchestrator is in its own git project remove the "make/" here
94+
yield os.path.join(workspace_root, "build/make/orchestrator/multitree_combos")
95+
96+
dirs = ["vendor", "device"]
97+
for d in dirs:
98+
yield from FindDirs(os.path.join(workspace_root, d), "multitree_combos")
99+
100+
101+
def FindNamedConfig(workspace_root, shortname):
102+
"""Find the config with the given shortname inside workspace_root.
103+
104+
Config directories are searched in the order described in FindConfigDirs,
105+
and inside those directories, alphabetically."""
106+
filename = shortname + ".mcombo"
107+
for config_dir in FindConfigDirs(workspace_root):
108+
found = FindFile(config_dir, filename)
109+
if found:
110+
return found
111+
return None
112+
113+
114+
def ParseProductVariant(s):
115+
"""Split a PRODUCT-VARIANT name, or return None if it doesn't match that pattern."""
116+
split = s.split("-")
117+
if len(split) != 2:
118+
return None
119+
return split
120+
121+
122+
def ChooseConfigFromArgs(workspace_root, args):
123+
"""Return the config file we should use for the given argument,
124+
or null if there's no file that matches that."""
125+
if len(args) == 1:
126+
# Prefer PRODUCT-VARIANT syntax so if there happens to be a matching
127+
# file we don't match that.
128+
pv = ParseProductVariant(args[0])
129+
if pv:
130+
config = FindNamedConfig(workspace_root, pv[0])
131+
if config:
132+
return (config, pv[1])
133+
return None, None
134+
# Look for a specifically named file
135+
if os.path.isfile(args[0]):
136+
return (args[0], args[1] if len(args) > 1 else None)
137+
# That file didn't exist, return that we didn't find it.
138+
return None, None
139+
140+
141+
class ConfigException(Exception):
142+
ERROR_PARSE = "parse"
143+
ERROR_CYCLE = "cycle"
144+
145+
def __init__(self, kind, message, locations, line=0):
146+
"""Error thrown when loading and parsing configurations.
147+
148+
Args:
149+
message: Error message to display to user
150+
locations: List of filenames of the include history. The 0 index one
151+
the location where the actual error occurred
152+
"""
153+
if len(locations):
154+
s = locations[0]
155+
if line:
156+
s += ":"
157+
s += str(line)
158+
s += ": "
159+
else:
160+
s = ""
161+
s += message
162+
if len(locations):
163+
for loc in locations[1:]:
164+
s += "\n included from %s" % loc
165+
super().__init__(s)
166+
self.kind = kind
167+
self.message = message
168+
self.locations = locations
169+
self.line = line
170+
171+
172+
def LoadConfig(filename):
173+
"""Load a config, including processing the inherits fields.
174+
175+
Raises:
176+
ConfigException on errors
177+
"""
178+
def LoadAndMerge(fn, visited):
179+
with open(fn) as f:
180+
try:
181+
contents = json.load(f)
182+
except json.decoder.JSONDecodeError as ex:
183+
if True:
184+
raise ConfigException(ConfigException.ERROR_PARSE, ex.msg, visited, ex.lineno)
185+
else:
186+
sys.stderr.write("exception %s" % ex.__dict__)
187+
raise ex
188+
# Merge all the parents into one data, with first-wins policy
189+
inherited_data = {}
190+
for parent in contents.get("inherits", []):
191+
if parent in visited:
192+
raise ConfigException(ConfigException.ERROR_CYCLE, "Cycle detected in inherits",
193+
visited)
194+
DeepMerge(inherited_data, LoadAndMerge(parent, [parent,] + visited))
195+
# Then merge inherited_data into contents, but what's already there will win.
196+
DeepMerge(contents, inherited_data)
197+
contents.pop("inherits", None)
198+
return contents
199+
return LoadAndMerge(filename, [filename,])
200+
201+
202+
def DeepMerge(merged, addition):
203+
"""Merge all fields of addition into merged. Pre-existing fields win."""
204+
for k, v in addition.items():
205+
if k in merged:
206+
if isinstance(v, dict) and isinstance(merged[k], dict):
207+
DeepMerge(merged[k], v)
208+
else:
209+
merged[k] = v
210+
211+
212+
def Lunch(args):
213+
"""Handle the lunch command."""
214+
# Check that we're at the top of a multitree workspace
215+
# TODO: Choose the right sentinel file
216+
if not os.path.exists("build/make/orchestrator"):
217+
sys.stderr.write("ERROR: lunch.py must be run from the root of a multi-tree workspace\n")
218+
return EXIT_STATUS_ERROR
219+
220+
# Choose the config file
221+
config_file, variant = ChooseConfigFromArgs(".", args)
222+
223+
if config_file == None:
224+
sys.stderr.write("Can't find lunch combo file for: %s\n" % " ".join(args))
225+
return EXIT_STATUS_NEED_HELP
226+
if variant == None:
227+
sys.stderr.write("Can't find variant for: %s\n" % " ".join(args))
228+
return EXIT_STATUS_NEED_HELP
229+
230+
# Parse the config file
231+
try:
232+
config = LoadConfig(config_file)
233+
except ConfigException as ex:
234+
sys.stderr.write(str(ex))
235+
return EXIT_STATUS_ERROR
236+
237+
# Fail if the lunchable bit isn't set, because this isn't a usable config
238+
if not config.get("lunchable", False):
239+
sys.stderr.write("%s: Lunch config file (or inherited files) does not have the 'lunchable'"
240+
% config_file)
241+
sys.stderr.write(" flag set, which means it is probably not a complete lunch spec.\n")
242+
243+
# All the validation has passed, so print the name of the file and the variant
244+
sys.stdout.write("%s\n" % config_file)
245+
sys.stdout.write("%s\n" % variant)
246+
247+
return EXIT_STATUS_OK
248+
249+
250+
def FindAllComboFiles(workspace_root):
251+
"""Find all .mcombo files in the prescribed locations in the tree."""
252+
for dir in FindConfigDirs(workspace_root):
253+
for file in WalkPaths(dir, lambda x: x.endswith(".mcombo")):
254+
yield file
255+
256+
257+
def IsFileLunchable(config_file):
258+
"""Parse config_file, flatten the inheritance, and return whether it can be
259+
used as a lunch target."""
260+
try:
261+
config = LoadConfig(config_file)
262+
except ConfigException as ex:
263+
sys.stderr.write("%s" % ex)
264+
return False
265+
return config.get("lunchable", False)
266+
267+
268+
def FindAllLunchable(workspace_root):
269+
"""Find all mcombo files in the tree (rooted at workspace_root) that when
270+
parsed (and inheritance is flattened) have lunchable: true."""
271+
for f in [x for x in FindAllComboFiles(workspace_root) if IsFileLunchable(x)]:
272+
yield f
273+
274+
275+
def List():
276+
"""Handle the --list command."""
277+
for f in sorted(FindAllLunchable(".")):
278+
print(f)
279+
280+
281+
def Print(args):
282+
"""Handle the --print command."""
283+
# Parse args
284+
if len(args) == 0:
285+
config_file = os.environ.get("TARGET_BUILD_COMBO")
286+
if not config_file:
287+
sys.stderr.write("TARGET_BUILD_COMBO not set. Run lunch or pass a combo file.\n")
288+
return EXIT_STATUS_NEED_HELP
289+
elif len(args) == 1:
290+
config_file = args[0]
291+
else:
292+
return EXIT_STATUS_NEED_HELP
293+
294+
# Parse the config file
295+
try:
296+
config = LoadConfig(config_file)
297+
except ConfigException as ex:
298+
sys.stderr.write(str(ex))
299+
return EXIT_STATUS_ERROR
300+
301+
# Print the config in json form
302+
json.dump(config, sys.stdout, indent=4)
303+
304+
return EXIT_STATUS_OK
305+
306+
307+
def main(argv):
308+
if len(argv) < 2 or argv[1] == "-h" or argv[1] == "--help":
309+
return EXIT_STATUS_NEED_HELP
310+
311+
if len(argv) == 2 and argv[1] == "--list":
312+
List()
313+
return EXIT_STATUS_OK
314+
315+
if len(argv) == 2 and argv[1] == "--print":
316+
return Print(argv[2:])
317+
return EXIT_STATUS_OK
318+
319+
if (len(argv) == 2 or len(argv) == 3) and argv[1] == "--lunch":
320+
return Lunch(argv[2:])
321+
322+
sys.stderr.write("Unknown lunch command: %s\n" % " ".join(argv[1:]))
323+
return EXIT_STATUS_NEED_HELP
324+
325+
if __name__ == "__main__":
326+
sys.exit(main(sys.argv))
327+
328+
329+
# vim: sts=4:ts=4:sw=4
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
a
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INVALID FILE

0 commit comments

Comments
 (0)