• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 main
16
17import (
18	"context"
19	"encoding/json"
20	"flag"
21	"fmt"
22	"io/ioutil"
23	"os"
24	"path/filepath"
25	"strconv"
26	"strings"
27	"time"
28
29	"android/soong/shared"
30	"android/soong/ui/build"
31	"android/soong/ui/logger"
32	"android/soong/ui/metrics"
33	"android/soong/ui/status"
34	"android/soong/ui/terminal"
35	"android/soong/ui/tracer"
36)
37
38const (
39	configDir  = "vendor/google/tools/soong_config"
40	jsonSuffix = "json"
41)
42
43// A command represents an operation to be executed in the soong build
44// system.
45type command struct {
46	// The flag name (must have double dashes).
47	flag string
48
49	// Description for the flag (to display when running help).
50	description string
51
52	// Stream the build status output into the simple terminal mode.
53	simpleOutput bool
54
55	// Sets a prefix string to use for filenames of log files.
56	logsPrefix string
57
58	// Creates the build configuration based on the args and build context.
59	config func(ctx build.Context, args ...string) build.Config
60
61	// Returns what type of IO redirection this Command requires.
62	stdio func() terminal.StdioInterface
63
64	// run the command
65	run func(ctx build.Context, config build.Config, args []string, logsDir string)
66}
67
68// list of supported commands (flags) supported by soong ui
69var commands []command = []command{
70	{
71		flag:        "--make-mode",
72		description: "build the modules by the target name (i.e. soong_docs)",
73		config: func(ctx build.Context, args ...string) build.Config {
74			return build.NewConfig(ctx, args...)
75		},
76		stdio: stdio,
77		run:   runMake,
78	}, {
79		flag:         "--dumpvar-mode",
80		description:  "print the value of the legacy make variable VAR to stdout",
81		simpleOutput: true,
82		logsPrefix:   "dumpvars-",
83		config:       dumpVarConfig,
84		stdio:        customStdio,
85		run:          dumpVar,
86	}, {
87		flag:         "--dumpvars-mode",
88		description:  "dump the values of one or more legacy make variables, in shell syntax",
89		simpleOutput: true,
90		logsPrefix:   "dumpvars-",
91		config:       dumpVarConfig,
92		stdio:        customStdio,
93		run:          dumpVars,
94	}, {
95		flag:        "--build-mode",
96		description: "build modules based on the specified build action",
97		config:      buildActionConfig,
98		stdio:       stdio,
99		run:         runMake,
100	},
101}
102
103// indexList returns the index of first found s. -1 is return if s is not
104// found.
105func indexList(s string, list []string) int {
106	for i, l := range list {
107		if l == s {
108			return i
109		}
110	}
111	return -1
112}
113
114// inList returns true if one or more of s is in the list.
115func inList(s string, list []string) bool {
116	return indexList(s, list) != -1
117}
118
119func loadEnvConfig() error {
120	bc := os.Getenv("ANDROID_BUILD_ENVIRONMENT_CONFIG")
121	if bc == "" {
122		return nil
123	}
124	cfgFile := filepath.Join(os.Getenv("TOP"), configDir, fmt.Sprintf("%s.%s", bc, jsonSuffix))
125
126	envVarsJSON, err := ioutil.ReadFile(cfgFile)
127	if err != nil {
128		fmt.Fprintf(os.Stderr, "\033[33mWARNING:\033[0m failed to open config file %s: %s\n", cfgFile, err.Error())
129		return nil
130	}
131
132	var envVars map[string]map[string]string
133	if err := json.Unmarshal(envVarsJSON, &envVars); err != nil {
134		return fmt.Errorf("env vars config file: %s did not parse correctly: %s", cfgFile, err.Error())
135	}
136	for k, v := range envVars["env"] {
137		if os.Getenv(k) != "" {
138			continue
139		}
140		if err := os.Setenv(k, v); err != nil {
141			return err
142		}
143	}
144	return nil
145}
146
147// Main execution of soong_ui. The command format is as follows:
148//
149//    soong_ui <command> [<arg 1> <arg 2> ... <arg n>]
150//
151// Command is the type of soong_ui execution. Only one type of
152// execution is specified. The args are specific to the command.
153func main() {
154	shared.ReexecWithDelveMaybe(os.Getenv("SOONG_UI_DELVE"), shared.ResolveDelveBinary())
155
156	buildStarted := time.Now()
157
158	c, args, err := getCommand(os.Args)
159	if err != nil {
160		fmt.Fprintf(os.Stderr, "Error parsing `soong` args: %s.\n", err)
161		os.Exit(1)
162	}
163
164	// Create a terminal output that mimics Ninja's.
165	output := terminal.NewStatusOutput(c.stdio().Stdout(), os.Getenv("NINJA_STATUS"), c.simpleOutput,
166		build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
167
168	// Attach a new logger instance to the terminal output.
169	log := logger.New(output)
170	defer log.Cleanup()
171
172	// Create a context to simplify the program termination process.
173	ctx, cancel := context.WithCancel(context.Background())
174	defer cancel()
175
176	// Create a new trace file writer, making it log events to the log instance.
177	trace := tracer.New(log)
178	defer trace.Close()
179
180	// Create and start a new metric record.
181	met := metrics.New()
182	met.SetBuildDateTime(buildStarted)
183	met.SetBuildCommand(os.Args)
184
185	// Create a new Status instance, which manages action counts and event output channels.
186	stat := &status.Status{}
187	defer stat.Finish()
188	// Hook up the terminal output and tracer to Status.
189	stat.AddOutput(output)
190	stat.AddOutput(trace.StatusTracer())
191
192	// Set up a cleanup procedure in case the normal termination process doesn't work.
193	build.SetupSignals(log, cancel, func() {
194		trace.Close()
195		log.Cleanup()
196		stat.Finish()
197	})
198
199	buildCtx := build.Context{ContextImpl: &build.ContextImpl{
200		Context: ctx,
201		Logger:  log,
202		Metrics: met,
203		Tracer:  trace,
204		Writer:  output,
205		Status:  stat,
206	}}
207
208	config := c.config(buildCtx, args...)
209
210	if err := loadEnvConfig(); err != nil {
211		fmt.Fprintf(os.Stderr, "failed to parse env config files: %v", err)
212		os.Exit(1)
213	}
214
215
216	build.SetupOutDir(buildCtx, config)
217
218	if config.UseBazel() && config.Dist() {
219		defer populateExternalDistDir(buildCtx, config)
220	}
221
222	// Set up files to be outputted in the log directory.
223	logsDir := config.LogsDir()
224
225	// Common list of metric file definition.
226	buildErrorFile := filepath.Join(logsDir, c.logsPrefix+"build_error")
227	rbeMetricsFile := filepath.Join(logsDir, c.logsPrefix+"rbe_metrics.pb")
228	soongMetricsFile := filepath.Join(logsDir, c.logsPrefix+"soong_metrics")
229
230	build.PrintOutDirWarning(buildCtx, config)
231
232	os.MkdirAll(logsDir, 0777)
233	log.SetOutput(filepath.Join(logsDir, c.logsPrefix+"soong.log"))
234	trace.SetOutput(filepath.Join(logsDir, c.logsPrefix+"build.trace"))
235	stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, c.logsPrefix+"verbose.log")))
236	stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, c.logsPrefix+"error.log")))
237	stat.AddOutput(status.NewProtoErrorLog(log, buildErrorFile))
238	stat.AddOutput(status.NewCriticalPath(log))
239	stat.AddOutput(status.NewBuildProgressLog(log, filepath.Join(logsDir, c.logsPrefix+"build_progress.pb")))
240
241	buildCtx.Verbosef("Detected %.3v GB total RAM", float32(config.TotalRAM())/(1024*1024*1024))
242	buildCtx.Verbosef("Parallelism (local/remote/highmem): %v/%v/%v",
243		config.Parallel(), config.RemoteParallel(), config.HighmemParallel())
244
245	{
246		// The order of the function calls is important. The last defer function call
247		// is the first one that is executed to save the rbe metrics to a protobuf
248		// file. The soong metrics file is then next. Bazel profiles are written
249		// before the uploadMetrics is invoked. The written files are then uploaded
250		// if the uploading of the metrics is enabled.
251		files := []string{
252			buildErrorFile,           // build error strings
253			rbeMetricsFile,           // high level metrics related to remote build execution.
254			soongMetricsFile,         // high level metrics related to this build system.
255			config.BazelMetricsDir(), // directory that contains a set of bazel metrics.
256		}
257		defer build.UploadMetrics(buildCtx, config, c.simpleOutput, buildStarted, files...)
258		defer met.Dump(soongMetricsFile)
259		defer build.DumpRBEMetrics(buildCtx, config, rbeMetricsFile)
260	}
261
262	// Read the time at the starting point.
263	if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
264		// soong_ui.bash uses the date command's %N (nanosec) flag when getting the start time,
265		// which Darwin doesn't support. Check if it was executed properly before parsing the value.
266		if !strings.HasSuffix(start, "N") {
267			if start_time, err := strconv.ParseUint(start, 10, 64); err == nil {
268				log.Verbosef("Took %dms to start up.",
269					time.Since(time.Unix(0, int64(start_time))).Nanoseconds()/time.Millisecond.Nanoseconds())
270				buildCtx.CompleteTrace(metrics.RunSetupTool, "startup", start_time, uint64(time.Now().UnixNano()))
271			}
272		}
273
274		if executable, err := os.Executable(); err == nil {
275			trace.ImportMicrofactoryLog(filepath.Join(filepath.Dir(executable), "."+filepath.Base(executable)+".trace"))
276		}
277	}
278
279	// Fix up the source tree due to a repo bug where it doesn't remove
280	// linkfiles that have been removed
281	fixBadDanglingLink(buildCtx, "hardware/qcom/sdm710/Android.bp")
282	fixBadDanglingLink(buildCtx, "hardware/qcom/sdm710/Android.mk")
283
284	// Create a source finder.
285	f := build.NewSourceFinder(buildCtx, config)
286	defer f.Shutdown()
287	build.FindSources(buildCtx, config, f)
288
289	c.run(buildCtx, config, args, logsDir)
290}
291
292func fixBadDanglingLink(ctx build.Context, name string) {
293	_, err := os.Lstat(name)
294	if err != nil {
295		return
296	}
297	_, err = os.Stat(name)
298	if os.IsNotExist(err) {
299		err = os.Remove(name)
300		if err != nil {
301			ctx.Fatalf("Failed to remove dangling link %q: %v", name, err)
302		}
303	}
304}
305
306func dumpVar(ctx build.Context, config build.Config, args []string, _ string) {
307	flags := flag.NewFlagSet("dumpvar", flag.ExitOnError)
308	flags.Usage = func() {
309		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvar-mode [--abs] <VAR>\n\n", os.Args[0])
310		fmt.Fprintln(ctx.Writer, "In dumpvar mode, print the value of the legacy make variable VAR to stdout")
311		fmt.Fprintln(ctx.Writer, "")
312
313		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that prints the human-readable config banner")
314		fmt.Fprintln(ctx.Writer, "from the beginning of the build.")
315		fmt.Fprintln(ctx.Writer, "")
316		flags.PrintDefaults()
317	}
318	abs := flags.Bool("abs", false, "Print the absolute path of the value")
319	flags.Parse(args)
320
321	if flags.NArg() != 1 {
322		flags.Usage()
323		os.Exit(1)
324	}
325
326	varName := flags.Arg(0)
327	if varName == "report_config" {
328		varData, err := build.DumpMakeVars(ctx, config, nil, build.BannerVars)
329		if err != nil {
330			ctx.Fatal(err)
331		}
332
333		fmt.Println(build.Banner(varData))
334	} else {
335		varData, err := build.DumpMakeVars(ctx, config, nil, []string{varName})
336		if err != nil {
337			ctx.Fatal(err)
338		}
339
340		if *abs {
341			var res []string
342			for _, path := range strings.Fields(varData[varName]) {
343				if abs, err := filepath.Abs(path); err == nil {
344					res = append(res, abs)
345				} else {
346					ctx.Fatalln("Failed to get absolute path of", path, err)
347				}
348			}
349			fmt.Println(strings.Join(res, " "))
350		} else {
351			fmt.Println(varData[varName])
352		}
353	}
354}
355
356func dumpVars(ctx build.Context, config build.Config, args []string, _ string) {
357	flags := flag.NewFlagSet("dumpvars", flag.ExitOnError)
358	flags.Usage = func() {
359		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvars-mode [--vars=\"VAR VAR ...\"]\n\n", os.Args[0])
360		fmt.Fprintln(ctx.Writer, "In dumpvars mode, dump the values of one or more legacy make variables, in")
361		fmt.Fprintln(ctx.Writer, "shell syntax. The resulting output may be sourced directly into a shell to")
362		fmt.Fprintln(ctx.Writer, "set corresponding shell variables.")
363		fmt.Fprintln(ctx.Writer, "")
364
365		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that dumps a variable containing the")
366		fmt.Fprintln(ctx.Writer, "human-readable config banner from the beginning of the build.")
367		fmt.Fprintln(ctx.Writer, "")
368		flags.PrintDefaults()
369	}
370
371	varsStr := flags.String("vars", "", "Space-separated list of variables to dump")
372	absVarsStr := flags.String("abs-vars", "", "Space-separated list of variables to dump (using absolute paths)")
373
374	varPrefix := flags.String("var-prefix", "", "String to prepend to all variable names when dumping")
375	absVarPrefix := flags.String("abs-var-prefix", "", "String to prepent to all absolute path variable names when dumping")
376
377	flags.Parse(args)
378
379	if flags.NArg() != 0 {
380		flags.Usage()
381		os.Exit(1)
382	}
383
384	vars := strings.Fields(*varsStr)
385	absVars := strings.Fields(*absVarsStr)
386
387	allVars := append([]string{}, vars...)
388	allVars = append(allVars, absVars...)
389
390	if i := indexList("report_config", allVars); i != -1 {
391		allVars = append(allVars[:i], allVars[i+1:]...)
392		allVars = append(allVars, build.BannerVars...)
393	}
394
395	if len(allVars) == 0 {
396		return
397	}
398
399	varData, err := build.DumpMakeVars(ctx, config, nil, allVars)
400	if err != nil {
401		ctx.Fatal(err)
402	}
403
404	for _, name := range vars {
405		if name == "report_config" {
406			fmt.Printf("%sreport_config='%s'\n", *varPrefix, build.Banner(varData))
407		} else {
408			fmt.Printf("%s%s='%s'\n", *varPrefix, name, varData[name])
409		}
410	}
411	for _, name := range absVars {
412		var res []string
413		for _, path := range strings.Fields(varData[name]) {
414			abs, err := filepath.Abs(path)
415			if err != nil {
416				ctx.Fatalln("Failed to get absolute path of", path, err)
417			}
418			res = append(res, abs)
419		}
420		fmt.Printf("%s%s='%s'\n", *absVarPrefix, name, strings.Join(res, " "))
421	}
422}
423
424func stdio() terminal.StdioInterface {
425	return terminal.StdioImpl{}
426}
427
428// dumpvar and dumpvars use stdout to output variable values, so use stderr instead of stdout when
429// reporting events to keep stdout clean from noise.
430func customStdio() terminal.StdioInterface {
431	return terminal.NewCustomStdio(os.Stdin, os.Stderr, os.Stderr)
432}
433
434// dumpVarConfig does not require any arguments to be parsed by the NewConfig.
435func dumpVarConfig(ctx build.Context, args ...string) build.Config {
436	return build.NewConfig(ctx)
437}
438
439func buildActionConfig(ctx build.Context, args ...string) build.Config {
440	flags := flag.NewFlagSet("build-mode", flag.ContinueOnError)
441	flags.Usage = func() {
442		fmt.Fprintf(ctx.Writer, "usage: %s --build-mode --dir=<path> <build action> [<build arg 1> <build arg 2> ...]\n\n", os.Args[0])
443		fmt.Fprintln(ctx.Writer, "In build mode, build the set of modules based on the specified build")
444		fmt.Fprintln(ctx.Writer, "action. The --dir flag is required to determine what is needed to")
445		fmt.Fprintln(ctx.Writer, "build in the source tree based on the build action. See below for")
446		fmt.Fprintln(ctx.Writer, "the list of acceptable build action flags.")
447		fmt.Fprintln(ctx.Writer, "")
448		flags.PrintDefaults()
449	}
450
451	buildActionFlags := []struct {
452		name        string
453		description string
454		action      build.BuildAction
455		set         bool
456	}{{
457		name:        "all-modules",
458		description: "Build action: build from the top of the source tree.",
459		action:      build.BUILD_MODULES,
460	}, {
461		// This is redirecting to mma build command behaviour. Once it has soaked for a
462		// while, the build command is deleted from here once it has been removed from the
463		// envsetup.sh.
464		name:        "modules-in-a-dir-no-deps",
465		description: "Build action: builds all of the modules in the current directory without their dependencies.",
466		action:      build.BUILD_MODULES_IN_A_DIRECTORY,
467	}, {
468		// This is redirecting to mmma build command behaviour. Once it has soaked for a
469		// while, the build command is deleted from here once it has been removed from the
470		// envsetup.sh.
471		name:        "modules-in-dirs-no-deps",
472		description: "Build action: builds all of the modules in the supplied directories without their dependencies.",
473		action:      build.BUILD_MODULES_IN_DIRECTORIES,
474	}, {
475		name:        "modules-in-a-dir",
476		description: "Build action: builds all of the modules in the current directory and their dependencies.",
477		action:      build.BUILD_MODULES_IN_A_DIRECTORY,
478	}, {
479		name:        "modules-in-dirs",
480		description: "Build action: builds all of the modules in the supplied directories and their dependencies.",
481		action:      build.BUILD_MODULES_IN_DIRECTORIES,
482	}}
483	for i, flag := range buildActionFlags {
484		flags.BoolVar(&buildActionFlags[i].set, flag.name, false, flag.description)
485	}
486	dir := flags.String("dir", "", "Directory of the executed build command.")
487
488	// Only interested in the first two args which defines the build action and the directory.
489	// The remaining arguments are passed down to the config.
490	const numBuildActionFlags = 2
491	if len(args) < numBuildActionFlags {
492		flags.Usage()
493		ctx.Fatalln("Improper build action arguments.")
494	}
495	flags.Parse(args[0:numBuildActionFlags])
496
497	// The next block of code is to validate that exactly one build action is set and the dir flag
498	// is specified.
499	buildActionCount := 0
500	var buildAction build.BuildAction
501	for _, flag := range buildActionFlags {
502		if flag.set {
503			buildActionCount++
504			buildAction = flag.action
505		}
506	}
507	if buildActionCount != 1 {
508		ctx.Fatalln("Build action not defined.")
509	}
510	if *dir == "" {
511		ctx.Fatalln("-dir not specified.")
512	}
513
514	// Remove the build action flags from the args as they are not recognized by the config.
515	args = args[numBuildActionFlags:]
516	return build.NewBuildActionConfig(buildAction, *dir, ctx, args...)
517}
518
519func runMake(ctx build.Context, config build.Config, _ []string, logsDir string) {
520	if config.IsVerbose() {
521		writer := ctx.Writer
522		fmt.Fprintln(writer, "! The argument `showcommands` is no longer supported.")
523		fmt.Fprintln(writer, "! Instead, the verbose log is always written to a compressed file in the output dir:")
524		fmt.Fprintln(writer, "!")
525		fmt.Fprintf(writer, "!   gzip -cd %s/verbose.log.gz | less -R\n", logsDir)
526		fmt.Fprintln(writer, "!")
527		fmt.Fprintln(writer, "! Older versions are saved in verbose.log.#.gz files")
528		fmt.Fprintln(writer, "")
529		select {
530		case <-time.After(5 * time.Second):
531		case <-ctx.Done():
532			return
533		}
534	}
535
536	if _, ok := config.Environment().Get("ONE_SHOT_MAKEFILE"); ok {
537		writer := ctx.Writer
538		fmt.Fprintln(writer, "! The variable `ONE_SHOT_MAKEFILE` is obsolete.")
539		fmt.Fprintln(writer, "!")
540		fmt.Fprintln(writer, "! If you're using `mm`, you'll need to run `source build/envsetup.sh` to update.")
541		fmt.Fprintln(writer, "!")
542		fmt.Fprintln(writer, "! Otherwise, either specify a module name with m, or use mma / MODULES-IN-...")
543		fmt.Fprintln(writer, "")
544		ctx.Fatal("done")
545	}
546
547	build.Build(ctx, config)
548}
549
550// getCommand finds the appropriate command based on args[1] flag. args[0]
551// is the soong_ui filename.
552func getCommand(args []string) (*command, []string, error) {
553	if len(args) < 2 {
554		return nil, nil, fmt.Errorf("Too few arguments: %q", args)
555	}
556
557	for _, c := range commands {
558		if c.flag == args[1] {
559			return &c, args[2:], nil
560		}
561	}
562
563	// command not found
564	return nil, nil, fmt.Errorf("Command not found: %q", args)
565}
566
567// For Bazel support, this moves files and directories from e.g. out/dist/$f to DIST_DIR/$f if necessary.
568func populateExternalDistDir(ctx build.Context, config build.Config) {
569	// Make sure that internalDistDirPath and externalDistDirPath are both absolute paths, so we can compare them
570	var err error
571	var internalDistDirPath string
572	var externalDistDirPath string
573	if internalDistDirPath, err = filepath.Abs(config.DistDir()); err != nil {
574		ctx.Fatalf("Unable to find absolute path of %s: %s", internalDistDirPath, err)
575	}
576	if externalDistDirPath, err = filepath.Abs(config.RealDistDir()); err != nil {
577		ctx.Fatalf("Unable to find absolute path of %s: %s", externalDistDirPath, err)
578	}
579	if externalDistDirPath == internalDistDirPath {
580		return
581	}
582
583	// Make sure the internal DIST_DIR actually exists before trying to read from it
584	if _, err = os.Stat(internalDistDirPath); os.IsNotExist(err) {
585		ctx.Println("Skipping Bazel dist dir migration - nothing to do!")
586		return
587	}
588
589	// Make sure the external DIST_DIR actually exists before trying to write to it
590	if err = os.MkdirAll(externalDistDirPath, 0755); err != nil {
591		ctx.Fatalf("Unable to make directory %s: %s", externalDistDirPath, err)
592	}
593
594	ctx.Println("Populating external DIST_DIR...")
595
596	populateExternalDistDirHelper(ctx, config, internalDistDirPath, externalDistDirPath)
597}
598
599func populateExternalDistDirHelper(ctx build.Context, config build.Config, internalDistDirPath string, externalDistDirPath string) {
600	files, err := ioutil.ReadDir(internalDistDirPath)
601	if err != nil {
602		ctx.Fatalf("Can't read internal distdir %s: %s", internalDistDirPath, err)
603	}
604	for _, f := range files {
605		internalFilePath := filepath.Join(internalDistDirPath, f.Name())
606		externalFilePath := filepath.Join(externalDistDirPath, f.Name())
607
608		if f.IsDir() {
609			// Moving a directory - check if there is an existing directory to merge with
610			externalLstat, err := os.Lstat(externalFilePath)
611			if err != nil {
612				if !os.IsNotExist(err) {
613					ctx.Fatalf("Can't lstat external %s: %s", externalDistDirPath, err)
614				}
615				// Otherwise, if the error was os.IsNotExist, that's fine and we fall through to the rename at the bottom
616			} else {
617				if externalLstat.IsDir() {
618					// Existing dir - try to merge the directories?
619					populateExternalDistDirHelper(ctx, config, internalFilePath, externalFilePath)
620					continue
621				} else {
622					// Existing file being replaced with a directory. Delete the existing file...
623					if err := os.RemoveAll(externalFilePath); err != nil {
624						ctx.Fatalf("Unable to remove existing %s: %s", externalFilePath, err)
625					}
626				}
627			}
628		} else {
629			// Moving a file (not a dir) - delete any existing file or directory
630			if err := os.RemoveAll(externalFilePath); err != nil {
631				ctx.Fatalf("Unable to remove existing %s: %s", externalFilePath, err)
632			}
633		}
634
635		// The actual move - do a rename instead of a copy in order to save disk space.
636		if err := os.Rename(internalFilePath, externalFilePath); err != nil {
637			ctx.Fatalf("Unable to rename %s -> %s due to error %s", internalFilePath, externalFilePath, err)
638		}
639	}
640}
641