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