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