1// Copyright 2017 Google Inc. All rights reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package build
16
17import (
18	"crypto/md5"
19	"fmt"
20	"io/ioutil"
21	"os"
22	"os/user"
23	"path/filepath"
24	"strings"
25
26	"android/soong/ui/metrics"
27	"android/soong/ui/status"
28)
29
30var spaceSlashReplacer = strings.NewReplacer("/", "_", " ", "_")
31
32const katiBuildSuffix = ""
33const katiCleanspecSuffix = "-cleanspec"
34const katiPackageSuffix = "-package"
35
36// genKatiSuffix creates a filename suffix for kati-generated files so that we
37// can cache them based on their inputs. Such files include the generated Ninja
38// files and env.sh environment variable setup files.
39//
40// The filename suffix should encode all common changes to Kati inputs.
41// Currently that includes the TARGET_PRODUCT and kati-processed command line
42// arguments.
43func genKatiSuffix(ctx Context, config Config) {
44	// Construct the base suffix.
45	katiSuffix := "-" + config.TargetProduct()
46
47	// Append kati arguments to the suffix.
48	if args := config.KatiArgs(); len(args) > 0 {
49		katiSuffix += "-" + spaceSlashReplacer.Replace(strings.Join(args, "_"))
50	}
51
52	// If the suffix is too long, replace it with a md5 hash and write a
53	// file that contains the original suffix.
54	if len(katiSuffix) > 64 {
55		shortSuffix := "-" + fmt.Sprintf("%x", md5.Sum([]byte(katiSuffix)))
56		config.SetKatiSuffix(shortSuffix)
57
58		ctx.Verbosef("Kati ninja suffix too long: %q", katiSuffix)
59		ctx.Verbosef("Replacing with: %q", shortSuffix)
60
61		if err := ioutil.WriteFile(strings.TrimSuffix(config.KatiBuildNinjaFile(), "ninja")+"suf", []byte(katiSuffix), 0777); err != nil {
62			ctx.Println("Error writing suffix file:", err)
63		}
64	} else {
65		config.SetKatiSuffix(katiSuffix)
66	}
67}
68
69// Base function to construct and run the Kati command line with additional
70// arguments, and a custom function closure to mutate the environment Kati runs
71// in.
72func runKati(ctx Context, config Config, extraSuffix string, args []string, envFunc func(*Environment)) {
73	executable := config.PrebuiltBuildTool("ckati")
74	// cKati arguments.
75	args = append([]string{
76		// Instead of executing commands directly, generate a Ninja file.
77		"--ninja",
78		// Generate Ninja files in the output directory.
79		"--ninja_dir=" + config.OutDir(),
80		// Filename suffix of the generated Ninja file.
81		"--ninja_suffix=" + config.KatiSuffix() + extraSuffix,
82		// Remove common parts at the beginning of a Ninja file, like build_dir,
83		// local_pool and _kati_always_build_. Allows Kati to be run multiple
84		// times, with generated Ninja files combined in a single invocation
85		// using 'include'.
86		"--no_ninja_prelude",
87		// Support declaring phony outputs in AOSP Ninja.
88		"--use_ninja_phony_output",
89		// Support declaring symlink outputs in AOSP Ninja.
90		"--use_ninja_symlink_outputs",
91		// Regenerate the Ninja file if environment inputs have changed. e.g.
92		// CLI flags, .mk file timestamps, env vars, $(wildcard ..) and some
93		// $(shell ..) results.
94		"--regen",
95		// Skip '-include' directives starting with the specified path. Used to
96		// ignore generated .mk files.
97		"--ignore_optional_include=" + filepath.Join(config.OutDir(), "%.P"),
98		// Detect the use of $(shell echo ...).
99		"--detect_android_echo",
100		// Colorful ANSI-based warning and error messages.
101		"--color_warnings",
102		// Generate all targets, not just the top level requested ones.
103		"--gen_all_targets",
104		// Use the built-in emulator of GNU find for better file finding
105		// performance. Used with $(shell find ...).
106		"--use_find_emulator",
107		// Fail when the find emulator encounters problems.
108		"--werror_find_emulator",
109		// Do not provide any built-in rules.
110		"--no_builtin_rules",
111		// Fail when suffix rules are used.
112		"--werror_suffix_rules",
113		// Fail when a real target depends on a phony target.
114		"--werror_real_to_phony",
115		// Makes real_to_phony checks assume that any top-level or leaf
116		// dependencies that does *not* have a '/' in it is a phony target.
117		"--top_level_phony",
118		// Fail when a phony target contains slashes.
119		"--werror_phony_looks_real",
120		// Fail when writing to a read-only directory.
121		"--werror_writable",
122		// Print Kati's internal statistics, such as the number of variables,
123		// implicit/explicit/suffix rules, and so on.
124		"--kati_stats",
125	}, args...)
126
127	// Generate a minimal Ninja file.
128	//
129	// Used for build_test and multiproduct_kati, which runs Kati several
130	// hundred times for different configurations to test file generation logic.
131	// These can result in generating Ninja files reaching ~1GB or more,
132	// resulting in ~hundreds of GBs of writes.
133	//
134	// Since we don't care about executing the Ninja files in these test cases,
135	// generating the Ninja file content wastes time, so skip writing any
136	// information out with --empty_ninja_file.
137	//
138	// From https://github.com/google/kati/commit/87b8da7af2c8bea28b1d8ab17679453d859f96e5
139	if config.EmptyNinjaFile() {
140		args = append(args, "--empty_ninja_file")
141	}
142
143	// Apply 'local_pool' to to all rules that don't specify a pool.
144	if config.UseRemoteBuild() {
145		args = append(args, "--default_pool=local_pool")
146	}
147
148	cmd := Command(ctx, config, "ckati", executable, args...)
149
150	// Set up the nsjail sandbox.
151	cmd.Sandbox = katiSandbox
152
153	// Set up stdout and stderr.
154	pipe, err := cmd.StdoutPipe()
155	if err != nil {
156		ctx.Fatalln("Error getting output pipe for ckati:", err)
157	}
158	cmd.Stderr = cmd.Stdout
159
160	// Apply the caller's function closure to mutate the environment variables.
161	envFunc(cmd.Environment)
162
163	// Pass on various build environment metadata to Kati.
164	if _, ok := cmd.Environment.Get("BUILD_USERNAME"); !ok {
165		username := "unknown"
166		if u, err := user.Current(); err == nil {
167			username = u.Username
168		} else {
169			ctx.Println("Failed to get current user:", err)
170		}
171		cmd.Environment.Set("BUILD_USERNAME", username)
172	}
173
174	if _, ok := cmd.Environment.Get("BUILD_HOSTNAME"); !ok {
175		hostname, err := os.Hostname()
176		if err != nil {
177			ctx.Println("Failed to read hostname:", err)
178			hostname = "unknown"
179		}
180		cmd.Environment.Set("BUILD_HOSTNAME", hostname)
181	}
182
183	cmd.StartOrFatal()
184	// Set up the ToolStatus command line reader for Kati for a consistent UI
185	// for the user.
186	status.KatiReader(ctx.Status.StartTool(), pipe)
187	cmd.WaitOrFatal()
188}
189
190func runKatiBuild(ctx Context, config Config) {
191	ctx.BeginTrace(metrics.RunKati, "kati build")
192	defer ctx.EndTrace()
193
194	args := []string{
195		// Mark the output directory as writable.
196		"--writable", config.OutDir() + "/",
197		// Fail when encountering implicit rules. e.g.
198		// %.foo: %.bar
199		//   cp $< $@
200		"--werror_implicit_rules",
201		// Entry point for the Kati Ninja file generation.
202		"-f", "build/make/core/main.mk",
203	}
204
205	if !config.BuildBrokenDupRules() {
206		// Fail when redefining / duplicating a target.
207		args = append(args, "--werror_overriding_commands")
208	}
209
210	args = append(args, config.KatiArgs()...)
211
212	args = append(args,
213		// Location of the Make vars .mk file generated by Soong.
214		"SOONG_MAKEVARS_MK="+config.SoongMakeVarsMk(),
215		// Location of the Android.mk file generated by Soong. This
216		// file contains Soong modules represented as Kati modules,
217		// allowing Kati modules to depend on Soong modules.
218		"SOONG_ANDROID_MK="+config.SoongAndroidMk(),
219		// Directory containing outputs for the target device.
220		"TARGET_DEVICE_DIR="+config.TargetDeviceDir(),
221		// Directory containing .mk files for packaging purposes, such as
222		// the dist.mk file, containing dist-for-goals data.
223		"KATI_PACKAGE_MK_DIR="+config.KatiPackageMkDir())
224
225	runKati(ctx, config, katiBuildSuffix, args, func(env *Environment) {})
226
227	// compress and dist the main build ninja file.
228	distGzipFile(ctx, config, config.KatiBuildNinjaFile())
229
230	// Cleanup steps.
231	cleanCopyHeaders(ctx, config)
232	cleanOldInstalledFiles(ctx, config)
233}
234
235// Clean out obsolete header files on the disk that were *not copied* during the
236// build with BUILD_COPY_HEADERS and LOCAL_COPY_HEADERS.
237//
238// These should be increasingly uncommon, as it's a deprecated feature and there
239// isn't an equivalent feature in Soong.
240func cleanCopyHeaders(ctx Context, config Config) {
241	ctx.BeginTrace("clean", "clean copy headers")
242	defer ctx.EndTrace()
243
244	// Read and parse the list of copied headers from a file in the product
245	// output directory.
246	data, err := ioutil.ReadFile(filepath.Join(config.ProductOut(), ".copied_headers_list"))
247	if err != nil {
248		if os.IsNotExist(err) {
249			return
250		}
251		ctx.Fatalf("Failed to read copied headers list: %v", err)
252	}
253
254	headers := strings.Fields(string(data))
255	if len(headers) < 1 {
256		ctx.Fatal("Failed to parse copied headers list: %q", string(data))
257	}
258	headerDir := headers[0]
259	headers = headers[1:]
260
261	// Walk the tree and remove any headers that are not in the list of copied
262	// headers in the current build.
263	filepath.Walk(headerDir,
264		func(path string, info os.FileInfo, err error) error {
265			if err != nil {
266				return nil
267			}
268			if info.IsDir() {
269				return nil
270			}
271			if !inList(path, headers) {
272				ctx.Printf("Removing obsolete header %q", path)
273				if err := os.Remove(path); err != nil {
274					ctx.Fatalf("Failed to remove obsolete header %q: %v", path, err)
275				}
276			}
277			return nil
278		})
279}
280
281// Clean out any previously installed files from the disk that are not installed
282// in the current build.
283func cleanOldInstalledFiles(ctx Context, config Config) {
284	ctx.BeginTrace("clean", "clean old installed files")
285	defer ctx.EndTrace()
286
287	// We shouldn't be removing files from one side of the two-step asan builds
288	var suffix string
289	if v, ok := config.Environment().Get("SANITIZE_TARGET"); ok {
290		if sanitize := strings.Fields(v); inList("address", sanitize) {
291			suffix = "_asan"
292		}
293	}
294
295	cleanOldFiles(ctx, config.ProductOut(), ".installable_files"+suffix)
296
297	cleanOldFiles(ctx, config.HostOut(), ".installable_test_files")
298}
299
300// Generate the Ninja file containing the packaging command lines for the dist
301// dir.
302func runKatiPackage(ctx Context, config Config) {
303	ctx.BeginTrace(metrics.RunKati, "kati package")
304	defer ctx.EndTrace()
305
306	args := []string{
307		// Mark the dist dir as writable.
308		"--writable", config.DistDir() + "/",
309		// Fail when encountering implicit rules. e.g.
310		"--werror_implicit_rules",
311		// Fail when redefining / duplicating a target.
312		"--werror_overriding_commands",
313		// Entry point.
314		"-f", "build/make/packaging/main.mk",
315		// Directory containing .mk files for packaging purposes, such as
316		// the dist.mk file, containing dist-for-goals data.
317		"KATI_PACKAGE_MK_DIR=" + config.KatiPackageMkDir(),
318	}
319
320	// Run Kati against a restricted set of environment variables.
321	runKati(ctx, config, katiPackageSuffix, args, func(env *Environment) {
322		env.Allow([]string{
323			// Some generic basics
324			"LANG",
325			"LC_MESSAGES",
326			"PATH",
327			"PWD",
328			"TMPDIR",
329
330			// Tool configs
331			"ASAN_SYMBOLIZER_PATH",
332			"JAVA_HOME",
333			"PYTHONDONTWRITEBYTECODE",
334
335			// Build configuration
336			"ANDROID_BUILD_SHELL",
337			"DIST_DIR",
338			"OUT_DIR",
339		}...)
340
341		if config.Dist() {
342			env.Set("DIST", "true")
343			env.Set("DIST_DIR", config.DistDir())
344		}
345	})
346
347	// Compress and dist the packaging Ninja file.
348	distGzipFile(ctx, config, config.KatiPackageNinjaFile())
349}
350
351// Run Kati on the cleanspec files to clean the build.
352func runKatiCleanSpec(ctx Context, config Config) {
353	ctx.BeginTrace(metrics.RunKati, "kati cleanspec")
354	defer ctx.EndTrace()
355
356	runKati(ctx, config, katiCleanspecSuffix, []string{
357		// Fail when encountering implicit rules. e.g.
358		"--werror_implicit_rules",
359		// Fail when redefining / duplicating a target.
360		"--werror_overriding_commands",
361		// Entry point.
362		"-f", "build/make/core/cleanbuild.mk",
363		"SOONG_MAKEVARS_MK=" + config.SoongMakeVarsMk(),
364		"TARGET_DEVICE_DIR=" + config.TargetDeviceDir(),
365	}, func(env *Environment) {})
366}
367