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