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	if err := loadEnvConfig(); err != nil {
209		fmt.Fprintf(os.Stderr, "failed to parse env config files: %v", err)
210		os.Exit(1)
211	}
212
213	config := c.config(buildCtx, args...)
214
215	build.SetupOutDir(buildCtx, config)
216
217	if config.UseBazel() && config.Dist() {
218		defer populateExternalDistDir(buildCtx, config)
219	}
220
221	// Set up files to be outputted in the log directory.
222	logsDir := config.LogsDir()
223
224	// Common list of metric file definition.
225	buildErrorFile := filepath.Join(logsDir, c.logsPrefix+"build_error")
226	rbeMetricsFile := filepath.Join(logsDir, c.logsPrefix+"rbe_metrics.pb")
227	soongMetricsFile := filepath.Join(logsDir, c.logsPrefix+"soong_metrics")
228
229	build.PrintOutDirWarning(buildCtx, config)
230
231	os.MkdirAll(logsDir, 0777)
232	log.SetOutput(filepath.Join(logsDir, c.logsPrefix+"soong.log"))
233	trace.SetOutput(filepath.Join(logsDir, c.logsPrefix+"build.trace"))
234	stat.AddOutput(status.NewVerboseLog(log, filepath.Join(logsDir, c.logsPrefix+"verbose.log")))
235	stat.AddOutput(status.NewErrorLog(log, filepath.Join(logsDir, c.logsPrefix+"error.log")))
236	stat.AddOutput(status.NewProtoErrorLog(log, buildErrorFile))
237	stat.AddOutput(status.NewCriticalPath(log))
238	stat.AddOutput(status.NewBuildProgressLog(log, filepath.Join(logsDir, c.logsPrefix+"build_progress.pb")))
239
240	buildCtx.Verbosef("Detected %.3v GB total RAM", float32(config.TotalRAM())/(1024*1024*1024))
241	buildCtx.Verbosef("Parallelism (local/remote/highmem): %v/%v/%v",
242		config.Parallel(), config.RemoteParallel(), config.HighmemParallel())
243
244	{
245		// The order of the function calls is important. The last defer function call
246		// is the first one that is executed to save the rbe metrics to a protobuf
247		// file. The soong metrics file is then next. Bazel profiles are written
248		// before the uploadMetrics is invoked. The written files are then uploaded
249		// if the uploading of the metrics is enabled.
250		files := []string{
251			buildErrorFile,           // build error strings
252			rbeMetricsFile,           // high level metrics related to remote build execution.
253			soongMetricsFile,         // high level metrics related to this build system.
254			config.BazelMetricsDir(), // directory that contains a set of bazel metrics.
255		}
256		defer build.UploadMetrics(buildCtx, config, c.simpleOutput, buildStarted, files...)
257		defer met.Dump(soongMetricsFile)
258		defer build.DumpRBEMetrics(buildCtx, config, rbeMetricsFile)
259	}
260
261	// Read the time at the starting point.
262	if start, ok := os.LookupEnv("TRACE_BEGIN_SOONG"); ok {
263		// soong_ui.bash uses the date command's %N (nanosec) flag when getting the start time,
264		// which Darwin doesn't support. Check if it was executed properly before parsing the value.
265		if !strings.HasSuffix(start, "N") {
266			if start_time, err := strconv.ParseUint(start, 10, 64); err == nil {
267				log.Verbosef("Took %dms to start up.",
268					time.Since(time.Unix(0, int64(start_time))).Nanoseconds()/time.Millisecond.Nanoseconds())
269				buildCtx.CompleteTrace(metrics.RunSetupTool, "startup", start_time, uint64(time.Now().UnixNano()))
270			}
271		}
272
273		if executable, err := os.Executable(); err == nil {
274			trace.ImportMicrofactoryLog(filepath.Join(filepath.Dir(executable), "."+filepath.Base(executable)+".trace"))
275		}
276	}
277
278	// Fix up the source tree due to a repo bug where it doesn't remove
279	// linkfiles that have been removed
280	fixBadDanglingLink(buildCtx, "hardware/qcom/sdm710/Android.bp")
281	fixBadDanglingLink(buildCtx, "hardware/qcom/sdm710/Android.mk")
282
283	// Create a source finder.
284	f := build.NewSourceFinder(buildCtx, config)
285	defer f.Shutdown()
286	build.FindSources(buildCtx, config, f)
287
288	c.run(buildCtx, config, args, logsDir)
289}
290
291func fixBadDanglingLink(ctx build.Context, name string) {
292	_, err := os.Lstat(name)
293	if err != nil {
294		return
295	}
296	_, err = os.Stat(name)
297	if os.IsNotExist(err) {
298		err = os.Remove(name)
299		if err != nil {
300			ctx.Fatalf("Failed to remove dangling link %q: %v", name, err)
301		}
302	}
303}
304
305func dumpVar(ctx build.Context, config build.Config, args []string, _ string) {
306	flags := flag.NewFlagSet("dumpvar", flag.ExitOnError)
307	flags.Usage = func() {
308		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvar-mode [--abs] <VAR>\n\n", os.Args[0])
309		fmt.Fprintln(ctx.Writer, "In dumpvar mode, print the value of the legacy make variable VAR to stdout")
310		fmt.Fprintln(ctx.Writer, "")
311
312		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that prints the human-readable config banner")
313		fmt.Fprintln(ctx.Writer, "from the beginning of the build.")
314		fmt.Fprintln(ctx.Writer, "")
315		flags.PrintDefaults()
316	}
317	abs := flags.Bool("abs", false, "Print the absolute path of the value")
318	flags.Parse(args)
319
320	if flags.NArg() != 1 {
321		flags.Usage()
322		os.Exit(1)
323	}
324
325	varName := flags.Arg(0)
326	if varName == "report_config" {
327		varData, err := build.DumpMakeVars(ctx, config, nil, build.BannerVars)
328		if err != nil {
329			ctx.Fatal(err)
330		}
331
332		fmt.Println(build.Banner(varData))
333	} else {
334		varData, err := build.DumpMakeVars(ctx, config, nil, []string{varName})
335		if err != nil {
336			ctx.Fatal(err)
337		}
338
339		if *abs {
340			var res []string
341			for _, path := range strings.Fields(varData[varName]) {
342				if abs, err := filepath.Abs(path); err == nil {
343					res = append(res, abs)
344				} else {
345					ctx.Fatalln("Failed to get absolute path of", path, err)
346				}
347			}
348			fmt.Println(strings.Join(res, " "))
349		} else {
350			fmt.Println(varData[varName])
351		}
352	}
353}
354
355func dumpVars(ctx build.Context, config build.Config, args []string, _ string) {
356	flags := flag.NewFlagSet("dumpvars", flag.ExitOnError)
357	flags.Usage = func() {
358		fmt.Fprintf(ctx.Writer, "usage: %s --dumpvars-mode [--vars=\"VAR VAR ...\"]\n\n", os.Args[0])
359		fmt.Fprintln(ctx.Writer, "In dumpvars mode, dump the values of one or more legacy make variables, in")
360		fmt.Fprintln(ctx.Writer, "shell syntax. The resulting output may be sourced directly into a shell to")
361		fmt.Fprintln(ctx.Writer, "set corresponding shell variables.")
362		fmt.Fprintln(ctx.Writer, "")
363
364		fmt.Fprintln(ctx.Writer, "'report_config' is a special case that dumps a variable containing the")
365		fmt.Fprintln(ctx.Writer, "human-readable config banner from the beginning of the build.")
366		fmt.Fprintln(ctx.Writer, "")
367		flags.PrintDefaults()
368	}
369
370	varsStr := flags.String("vars", "", "Space-separated list of variables to dump")
371	absVarsStr := flags.String("abs-vars", "", "Space-separated list of variables to dump (using absolute paths)")
372
373	varPrefix := flags.String("var-prefix", "", "String to prepend to all variable names when dumping")
374	absVarPrefix := flags.String("abs-var-prefix", "", "String to prepent to all absolute path variable names when dumping")
375
376	flags.Parse(args)
377
378	if flags.NArg() != 0 {
379		flags.Usage()
380		os.Exit(1)
381	}
382
383	vars := strings.Fields(*varsStr)
384	absVars := strings.Fields(*absVarsStr)
385
386	allVars := append([]string{}, vars...)
387	allVars = append(allVars, absVars...)
388
389	if i := indexList("report_config", allVars); i != -1 {
390		allVars = append(allVars[:i], allVars[i+1:]...)
391		allVars = append(allVars, build.BannerVars...)
392	}
393
394	if len(allVars) == 0 {
395		return
396	}
397
398	varData, err := build.DumpMakeVars(ctx, config, nil, allVars)
399	if err != nil {
400		ctx.Fatal(err)
401	}
402
403	for _, name := range vars {
404		if name == "report_config" {
405			fmt.Printf("%sreport_config='%s'\n", *varPrefix, build.Banner(varData))
406		} else {
407			fmt.Printf("%s%s='%s'\n", *varPrefix, name, varData[name])
408		}
409	}
410	for _, name := range absVars {
411		var res []string
412		for _, path := range strings.Fields(varData[name]) {
413			abs, err := filepath.Abs(path)
414			if err != nil {
415				ctx.Fatalln("Failed to get absolute path of", path, err)
416			}
417			res = append(res, abs)
418		}
419		fmt.Printf("%s%s='%s'\n", *absVarPrefix, name, strings.Join(res, " "))
420	}
421}
422
423func stdio() terminal.StdioInterface {
424	return terminal.StdioImpl{}
425}
426
427// dumpvar and dumpvars use stdout to output variable values, so use stderr instead of stdout when
428// reporting events to keep stdout clean from noise.
429func customStdio() terminal.StdioInterface {
430	return terminal.NewCustomStdio(os.Stdin, os.Stderr, os.Stderr)
431}
432
433// dumpVarConfig does not require any arguments to be parsed by the NewConfig.
434func dumpVarConfig(ctx build.Context, args ...string) build.Config {
435	return build.NewConfig(ctx)
436}
437
438func buildActionConfig(ctx build.Context, args ...string) build.Config {
439	flags := flag.NewFlagSet("build-mode", flag.ContinueOnError)
440	flags.Usage = func() {
441		fmt.Fprintf(ctx.Writer, "usage: %s --build-mode --dir=<path> <build action> [<build arg 1> <build arg 2> ...]\n\n", os.Args[0])
442		fmt.Fprintln(ctx.Writer, "In build mode, build the set of modules based on the specified build")
443		fmt.Fprintln(ctx.Writer, "action. The --dir flag is required to determine what is needed to")
444		fmt.Fprintln(ctx.Writer, "build in the source tree based on the build action. See below for")
445		fmt.Fprintln(ctx.Writer, "the list of acceptable build action flags.")
446		fmt.Fprintln(ctx.Writer, "")
447		flags.PrintDefaults()
448	}
449
450	buildActionFlags := []struct {
451		name        string
452		description string
453		action      build.BuildAction
454		set         bool
455	}{{
456		name:        "all-modules",
457		description: "Build action: build from the top of the source tree.",
458		action:      build.BUILD_MODULES,
459	}, {
460		// This is redirecting to mma build command behaviour. Once it has soaked for a
461		// while, the build command is deleted from here once it has been removed from the
462		// envsetup.sh.
463		name:        "modules-in-a-dir-no-deps",
464		description: "Build action: builds all of the modules in the current directory without their dependencies.",
465		action:      build.BUILD_MODULES_IN_A_DIRECTORY,
466	}, {
467		// This is redirecting to mmma build command behaviour. Once it has soaked for a
468		// while, the build command is deleted from here once it has been removed from the
469		// envsetup.sh.
470		name:        "modules-in-dirs-no-deps",
471		description: "Build action: builds all of the modules in the supplied directories without their dependencies.",
472		action:      build.BUILD_MODULES_IN_DIRECTORIES,
473	}, {
474		name:        "modules-in-a-dir",
475		description: "Build action: builds all of the modules in the current directory and their dependencies.",
476		action:      build.BUILD_MODULES_IN_A_DIRECTORY,
477	}, {
478		name:        "modules-in-dirs",
479		description: "Build action: builds all of the modules in the supplied directories and their dependencies.",
480		action:      build.BUILD_MODULES_IN_DIRECTORIES,
481	}}
482	for i, flag := range buildActionFlags {
483		flags.BoolVar(&buildActionFlags[i].set, flag.name, false, flag.description)
484	}
485	dir := flags.String("dir", "", "Directory of the executed build command.")
486
487	// Only interested in the first two args which defines the build action and the directory.
488	// The remaining arguments are passed down to the config.
489	const numBuildActionFlags = 2
490	if len(args) < numBuildActionFlags {
491		flags.Usage()
492		ctx.Fatalln("Improper build action arguments.")
493	}
494	flags.Parse(args[0:numBuildActionFlags])
495
496	// The next block of code is to validate that exactly one build action is set and the dir flag
497	// is specified.
498	buildActionCount := 0
499	var buildAction build.BuildAction
500	for _, flag := range buildActionFlags {
501		if flag.set {
502			buildActionCount++
503			buildAction = flag.action
504		}
505	}
506	if buildActionCount != 1 {
507		ctx.Fatalln("Build action not defined.")
508	}
509	if *dir == "" {
510		ctx.Fatalln("-dir not specified.")
511	}
512
513	// Remove the build action flags from the args as they are not recognized by the config.
514	args = args[numBuildActionFlags:]
515	return build.NewBuildActionConfig(buildAction, *dir, ctx, args...)
516}
517
518func runMake(ctx build.Context, config build.Config, _ []string, logsDir string) {
519	if config.IsVerbose() {
520		writer := ctx.Writer
521		fmt.Fprintln(writer, "! The argument `showcommands` is no longer supported.")
522		fmt.Fprintln(writer, "! Instead, the verbose log is always written to a compressed file in the output dir:")
523		fmt.Fprintln(writer, "!")
524		fmt.Fprintf(writer, "!   gzip -cd %s/verbose.log.gz | less -R\n", logsDir)
525		fmt.Fprintln(writer, "!")
526		fmt.Fprintln(writer, "! Older versions are saved in verbose.log.#.gz files")
527		fmt.Fprintln(writer, "")
528		select {
529		case <-time.After(5 * time.Second):
530		case <-ctx.Done():
531			return
532		}
533	}
534
535	if _, ok := config.Environment().Get("ONE_SHOT_MAKEFILE"); ok {
536		writer := ctx.Writer
537		fmt.Fprintln(writer, "! The variable `ONE_SHOT_MAKEFILE` is obsolete.")
538		fmt.Fprintln(writer, "!")
539		fmt.Fprintln(writer, "! If you're using `mm`, you'll need to run `source build/envsetup.sh` to update.")
540		fmt.Fprintln(writer, "!")
541		fmt.Fprintln(writer, "! Otherwise, either specify a module name with m, or use mma / MODULES-IN-...")
542		fmt.Fprintln(writer, "")
543		ctx.Fatal("done")
544	}
545
546	build.Build(ctx, config)
547}
548
549// getCommand finds the appropriate command based on args[1] flag. args[0]
550// is the soong_ui filename.
551func getCommand(args []string) (*command, []string, error) {
552	if len(args) < 2 {
553		return nil, nil, fmt.Errorf("Too few arguments: %q", args)
554	}
555
556	for _, c := range commands {
557		if c.flag == args[1] {
558			return &c, args[2:], nil
559		}
560	}
561
562	// command not found
563	return nil, nil, fmt.Errorf("Command not found: %q", args)
564}
565
566// For Bazel support, this moves files and directories from e.g. out/dist/$f to DIST_DIR/$f if necessary.
567func populateExternalDistDir(ctx build.Context, config build.Config) {
568	// Make sure that internalDistDirPath and externalDistDirPath are both absolute paths, so we can compare them
569	var err error
570	var internalDistDirPath string
571	var externalDistDirPath string
572	if internalDistDirPath, err = filepath.Abs(config.DistDir()); err != nil {
573		ctx.Fatalf("Unable to find absolute path of %s: %s", internalDistDirPath, err)
574	}
575	if externalDistDirPath, err = filepath.Abs(config.RealDistDir()); err != nil {
576		ctx.Fatalf("Unable to find absolute path of %s: %s", externalDistDirPath, err)
577	}
578	if externalDistDirPath == internalDistDirPath {
579		return
580	}
581
582	// Make sure the internal DIST_DIR actually exists before trying to read from it
583	if _, err = os.Stat(internalDistDirPath); os.IsNotExist(err) {
584		ctx.Println("Skipping Bazel dist dir migration - nothing to do!")
585		return
586	}
587
588	// Make sure the external DIST_DIR actually exists before trying to write to it
589	if err = os.MkdirAll(externalDistDirPath, 0755); err != nil {
590		ctx.Fatalf("Unable to make directory %s: %s", externalDistDirPath, err)
591	}
592
593	ctx.Println("Populating external DIST_DIR...")
594
595	populateExternalDistDirHelper(ctx, config, internalDistDirPath, externalDistDirPath)
596}
597
598func populateExternalDistDirHelper(ctx build.Context, config build.Config, internalDistDirPath string, externalDistDirPath string) {
599	files, err := ioutil.ReadDir(internalDistDirPath)
600	if err != nil {
601		ctx.Fatalf("Can't read internal distdir %s: %s", internalDistDirPath, err)
602	}
603	for _, f := range files {
604		internalFilePath := filepath.Join(internalDistDirPath, f.Name())
605		externalFilePath := filepath.Join(externalDistDirPath, f.Name())
606
607		if f.IsDir() {
608			// Moving a directory - check if there is an existing directory to merge with
609			externalLstat, err := os.Lstat(externalFilePath)
610			if err != nil {
611				if !os.IsNotExist(err) {
612					ctx.Fatalf("Can't lstat external %s: %s", externalDistDirPath, err)
613				}
614				// Otherwise, if the error was os.IsNotExist, that's fine and we fall through to the rename at the bottom
615			} else {
616				if externalLstat.IsDir() {
617					// Existing dir - try to merge the directories?
618					populateExternalDistDirHelper(ctx, config, internalFilePath, externalFilePath)
619					continue
620				} else {
621					// Existing file being replaced with a directory. Delete the existing file...
622					if err := os.RemoveAll(externalFilePath); err != nil {
623						ctx.Fatalf("Unable to remove existing %s: %s", externalFilePath, err)
624					}
625				}
626			}
627		} else {
628			// Moving a file (not a dir) - delete any existing file or directory
629			if err := os.RemoveAll(externalFilePath); err != nil {
630				ctx.Fatalf("Unable to remove existing %s: %s", externalFilePath, err)
631			}
632		}
633
634		// The actual move - do a rename instead of a copy in order to save disk space.
635		if err := os.Rename(internalFilePath, externalFilePath); err != nil {
636			ctx.Fatalf("Unable to rename %s -> %s due to error %s", internalFilePath, externalFilePath, err)
637		}
638	}
639}
640