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