blob: 47712062f3255a3fb7db5775834072bb074230c3 [file] [log] [blame]
Dan Willemsenc2af0be2017-01-20 14:10:01 -08001// 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 (
Dan Willemsenc2af0be2017-01-20 14:10:01 -080018 "context"
19 "flag"
20 "fmt"
Dan Willemsen41538382018-08-31 19:51:35 -070021 "io"
Dan Willemsenf624fb92017-05-19 16:39:04 -070022 "io/ioutil"
Dan Willemsenc2af0be2017-01-20 14:10:01 -080023 "os"
24 "path/filepath"
25 "runtime"
26 "strings"
27 "sync"
Dan Willemsen22de2162017-12-11 14:35:23 -080028 "syscall"
Steven Moreland552432e2017-03-29 19:26:09 -070029 "time"
Dan Willemsenc2af0be2017-01-20 14:10:01 -080030
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -070031 "android/soong/finder"
Dan Willemsenc2af0be2017-01-20 14:10:01 -080032 "android/soong/ui/build"
33 "android/soong/ui/logger"
Dan Willemsenb82471a2018-05-17 16:37:09 -070034 "android/soong/ui/status"
35 "android/soong/ui/terminal"
Dan Willemsend9f6fa22016-08-21 15:17:17 -070036 "android/soong/ui/tracer"
Dan Willemsene3480762017-11-07 11:23:27 -080037 "android/soong/zip"
Dan Willemsenc2af0be2017-01-20 14:10:01 -080038)
39
40// We default to number of cpus / 4, which seems to be the sweet spot for my
41// system. I suspect this is mostly due to memory or disk bandwidth though, and
42// may depend on the size ofthe source tree, so this probably isn't a great
43// default.
44func detectNumJobs() int {
45 if runtime.NumCPU() < 4 {
46 return 1
47 }
48 return runtime.NumCPU() / 4
49}
50
51var numJobs = flag.Int("j", detectNumJobs(), "number of parallel kati jobs")
52
Dan Willemsene3480762017-11-07 11:23:27 -080053var keepArtifacts = flag.Bool("keep", false, "keep archives of artifacts")
Dan Willemsen41538382018-08-31 19:51:35 -070054var incremental = flag.Bool("incremental", false, "run in incremental mode (saving intermediates)")
Dan Willemsenc2af0be2017-01-20 14:10:01 -080055
56var outDir = flag.String("out", "", "path to store output directories (defaults to tmpdir under $OUT when empty)")
Dan Willemsenf624fb92017-05-19 16:39:04 -070057var alternateResultDir = flag.Bool("dist", false, "write select results to $DIST_DIR (or <out>/dist when empty)")
Dan Willemsenc2af0be2017-01-20 14:10:01 -080058
59var onlyConfig = flag.Bool("only-config", false, "Only run product config (not Soong or Kati)")
60var onlySoong = flag.Bool("only-soong", false, "Only run product config and Soong (not Kati)")
61
Dan Willemsen5ed900b2017-05-07 11:40:30 -070062var buildVariant = flag.String("variant", "eng", "build variant to use")
63
Dan Willemsen9957b9c2017-10-06 15:05:05 -070064var skipProducts = flag.String("skip-products", "", "comma-separated list of products to skip (known failures, etc)")
Jeff Gastonb61e3f72017-10-25 15:02:45 -070065var includeProducts = flag.String("products", "", "comma-separated list of products to build")
Dan Willemsen9957b9c2017-10-06 15:05:05 -070066
Dan Willemsen9609ad92019-12-05 15:22:41 -080067var shardCount = flag.Int("shard-count", 1, "split the products into multiple shards (to spread the build onto multiple machines, etc)")
68var shard = flag.Int("shard", 1, "1-indexed shard to execute")
69
Dan Willemsenf624fb92017-05-19 16:39:04 -070070const errorLeadingLines = 20
71const errorTrailingLines = 20
72
Dan Willemsenb82471a2018-05-17 16:37:09 -070073func errMsgFromLog(filename string) string {
74 if filename == "" {
75 return ""
Dan Willemsena4e43a72017-05-06 16:58:26 -070076 }
77
Dan Willemsenb82471a2018-05-17 16:37:09 -070078 data, err := ioutil.ReadFile(filename)
79 if err != nil {
80 return ""
Dan Willemsenf624fb92017-05-19 16:39:04 -070081 }
82
Dan Willemsenb82471a2018-05-17 16:37:09 -070083 lines := strings.Split(strings.TrimSpace(string(data)), "\n")
84 if len(lines) > errorLeadingLines+errorTrailingLines+1 {
85 lines[errorLeadingLines] = fmt.Sprintf("... skipping %d lines ...",
86 len(lines)-errorLeadingLines-errorTrailingLines)
Dan Willemsena4e43a72017-05-06 16:58:26 -070087
Dan Willemsenb82471a2018-05-17 16:37:09 -070088 lines = append(lines[:errorLeadingLines+1],
89 lines[len(lines)-errorTrailingLines:]...)
Dan Willemsena4e43a72017-05-06 16:58:26 -070090 }
Dan Willemsenb82471a2018-05-17 16:37:09 -070091 var buf strings.Builder
92 for _, line := range lines {
93 buf.WriteString("> ")
94 buf.WriteString(line)
95 buf.WriteString("\n")
Dan Willemsena4e43a72017-05-06 16:58:26 -070096 }
Dan Willemsenb82471a2018-05-17 16:37:09 -070097 return buf.String()
Dan Willemsena4e43a72017-05-06 16:58:26 -070098}
99
Dan Willemsen22de2162017-12-11 14:35:23 -0800100// TODO(b/70370883): This tool uses a lot of open files -- over the default
101// soft limit of 1024 on some systems. So bump up to the hard limit until I fix
102// the algorithm.
103func setMaxFiles(log logger.Logger) {
104 var limits syscall.Rlimit
105
106 err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limits)
107 if err != nil {
108 log.Println("Failed to get file limit:", err)
109 return
110 }
111
112 log.Verbosef("Current file limits: %d soft, %d hard", limits.Cur, limits.Max)
113 if limits.Cur == limits.Max {
114 return
115 }
116
117 limits.Cur = limits.Max
118 err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limits)
119 if err != nil {
120 log.Println("Failed to increase file limit:", err)
121 }
122}
123
Jeff Gastonb61e3f72017-10-25 15:02:45 -0700124func inList(str string, list []string) bool {
125 for _, other := range list {
126 if str == other {
127 return true
128 }
129 }
130 return false
131}
132
Dan Willemsen41538382018-08-31 19:51:35 -0700133func copyFile(from, to string) error {
134 fromFile, err := os.Open(from)
135 if err != nil {
136 return err
137 }
138 defer fromFile.Close()
139
140 toFile, err := os.Create(to)
141 if err != nil {
142 return err
143 }
144 defer toFile.Close()
145
146 _, err = io.Copy(toFile, fromFile)
147 return err
148}
149
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700150type mpContext struct {
151 Context context.Context
152 Logger logger.Logger
153 Status status.ToolStatus
154 Tracer tracer.Tracer
155 Finder *finder.Finder
156 Config build.Config
157
158 LogsDir string
159}
160
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800161func main() {
Colin Cross097ed2a2019-06-08 21:48:58 -0700162 stdio := terminal.StdioImpl{}
Dan Willemsenb82471a2018-05-17 16:37:09 -0700163
Colin Crossc0b9f6b2019-09-23 12:44:54 -0700164 output := terminal.NewStatusOutput(stdio.Stdout(), "", false,
Colin Crosse0df1a32019-06-09 19:40:08 -0700165 build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD"))
166
167 log := logger.New(output)
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800168 defer log.Cleanup()
169
170 flag.Parse()
171
172 ctx, cancel := context.WithCancel(context.Background())
173 defer cancel()
174
Dan Willemsend9f6fa22016-08-21 15:17:17 -0700175 trace := tracer.New(log)
176 defer trace.Close()
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800177
Dan Willemsenb82471a2018-05-17 16:37:09 -0700178 stat := &status.Status{}
179 defer stat.Finish()
Colin Crosse0df1a32019-06-09 19:40:08 -0700180 stat.AddOutput(output)
Dan Willemsenb82471a2018-05-17 16:37:09 -0700181
Dan Willemsen34218ec2018-07-19 22:57:18 -0700182 var failures failureCount
183 stat.AddOutput(&failures)
184
Dan Willemsend9f6fa22016-08-21 15:17:17 -0700185 build.SetupSignals(log, cancel, func() {
186 trace.Close()
187 log.Cleanup()
Dan Willemsenb82471a2018-05-17 16:37:09 -0700188 stat.Finish()
Dan Willemsend9f6fa22016-08-21 15:17:17 -0700189 })
190
Dan Willemsen59339a22018-07-22 21:18:45 -0700191 buildCtx := build.Context{ContextImpl: &build.ContextImpl{
Dan Willemsenb82471a2018-05-17 16:37:09 -0700192 Context: ctx,
193 Logger: log,
194 Tracer: trace,
Colin Crosse0df1a32019-06-09 19:40:08 -0700195 Writer: output,
Dan Willemsenb82471a2018-05-17 16:37:09 -0700196 Status: stat,
Dan Willemsend9f6fa22016-08-21 15:17:17 -0700197 }}
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800198
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800199 config := build.NewConfig(buildCtx)
200 if *outDir == "" {
Dan Willemsen41538382018-08-31 19:51:35 -0700201 name := "multiproduct"
202 if !*incremental {
203 name += "-" + time.Now().Format("20060102150405")
204 }
Steven Moreland552432e2017-03-29 19:26:09 -0700205
206 *outDir = filepath.Join(config.OutDir(), name)
207
Dan Willemsenf624fb92017-05-19 16:39:04 -0700208 // Ensure the empty files exist in the output directory
209 // containing our output directory too. This is mostly for
210 // safety, but also triggers the ninja_build file so that our
211 // build servers know that they can parse the output as if it
212 // was ninja output.
213 build.SetupOutDir(buildCtx, config)
214
Steven Moreland552432e2017-03-29 19:26:09 -0700215 if err := os.MkdirAll(*outDir, 0777); err != nil {
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800216 log.Fatalf("Failed to create tempdir: %v", err)
217 }
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800218 }
219 config.Environment().Set("OUT_DIR", *outDir)
220 log.Println("Output directory:", *outDir)
221
Dan Willemsene3480762017-11-07 11:23:27 -0800222 logsDir := filepath.Join(config.OutDir(), "logs")
223 os.MkdirAll(logsDir, 0777)
224
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800225 build.SetupOutDir(buildCtx, config)
Dan Willemsenf624fb92017-05-19 16:39:04 -0700226 if *alternateResultDir {
Dan Willemsene3480762017-11-07 11:23:27 -0800227 distLogsDir := filepath.Join(config.DistDir(), "logs")
228 os.MkdirAll(distLogsDir, 0777)
229 log.SetOutput(filepath.Join(distLogsDir, "soong.log"))
230 trace.SetOutput(filepath.Join(distLogsDir, "build.trace"))
Dan Willemsenf624fb92017-05-19 16:39:04 -0700231 } else {
232 log.SetOutput(filepath.Join(config.OutDir(), "soong.log"))
233 trace.SetOutput(filepath.Join(config.OutDir(), "build.trace"))
234 }
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800235
Dan Willemsen22de2162017-12-11 14:35:23 -0800236 setMaxFiles(log)
237
Dan Willemsen04d76ef2018-05-15 00:52:29 -0700238 finder := build.NewSourceFinder(buildCtx, config)
239 defer finder.Shutdown()
240
241 build.FindSources(buildCtx, config, finder)
242
Dan Willemsenb2e6c2e2017-07-13 17:24:44 -0700243 vars, err := build.DumpMakeVars(buildCtx, config, nil, []string{"all_named_products"})
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800244 if err != nil {
245 log.Fatal(err)
246 }
Jeff Gastonb61e3f72017-10-25 15:02:45 -0700247 var productsList []string
248 allProducts := strings.Fields(vars["all_named_products"])
249
250 if *includeProducts != "" {
251 missingProducts := []string{}
252 for _, product := range strings.Split(*includeProducts, ",") {
253 if inList(product, allProducts) {
254 productsList = append(productsList, product)
255 } else {
256 missingProducts = append(missingProducts, product)
257 }
258 }
259 if len(missingProducts) > 0 {
260 log.Fatalf("Products don't exist: %s\n", missingProducts)
261 }
262 } else {
263 productsList = allProducts
264 }
Dan Willemsen9957b9c2017-10-06 15:05:05 -0700265
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700266 finalProductsList := make([]string, 0, len(productsList))
Dan Willemsen9957b9c2017-10-06 15:05:05 -0700267 skipList := strings.Split(*skipProducts, ",")
268 skipProduct := func(p string) bool {
269 for _, s := range skipList {
270 if p == s {
271 return true
272 }
273 }
274 return false
275 }
276 for _, product := range productsList {
277 if !skipProduct(product) {
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700278 finalProductsList = append(finalProductsList, product)
Dan Willemsen9957b9c2017-10-06 15:05:05 -0700279 } else {
280 log.Verbose("Skipping: ", product)
281 }
282 }
283
Dan Willemsen9609ad92019-12-05 15:22:41 -0800284 if *shard < 1 {
285 log.Fatalf("--shard value must be >= 1, not %d\n", *shard)
286 } else if *shardCount < 1 {
287 log.Fatalf("--shard-count value must be >= 1, not %d\n", *shardCount)
288 } else if *shard > *shardCount {
289 log.Fatalf("--shard (%d) must not be greater than --shard-count (%d)\n", *shard,
290 *shardCount)
291 } else if *shardCount > 1 {
292 finalProductsList = splitList(finalProductsList, *shardCount)[*shard-1]
293 }
294
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700295 log.Verbose("Got product list: ", finalProductsList)
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800296
Dan Willemsenb82471a2018-05-17 16:37:09 -0700297 s := buildCtx.Status.StartTool()
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700298 s.SetTotalActions(len(finalProductsList))
Dan Willemsena4e43a72017-05-06 16:58:26 -0700299
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700300 mpCtx := &mpContext{
301 Context: ctx,
302 Logger: log,
303 Status: s,
304 Tracer: trace,
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800305
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700306 Finder: finder,
307 Config: config,
Dan Willemsenf624fb92017-05-19 16:39:04 -0700308
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700309 LogsDir: logsDir,
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800310 }
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700311
312 products := make(chan string, len(productsList))
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800313 go func() {
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700314 defer close(products)
315 for _, product := range finalProductsList {
316 products <- product
317 }
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800318 }()
319
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700320 var wg sync.WaitGroup
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800321 for i := 0; i < *numJobs; i++ {
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700322 wg.Add(1)
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800323 go func() {
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700324 defer wg.Done()
325 for {
326 select {
327 case product := <-products:
328 if product == "" {
329 return
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800330 }
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700331 buildProduct(mpCtx, product)
332 }
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800333 }
334 }()
335 }
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700336 wg.Wait()
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800337
Dan Willemsene3480762017-11-07 11:23:27 -0800338 if *alternateResultDir {
339 args := zip.ZipArgs{
340 FileArgs: []zip.FileArg{
341 {GlobDir: logsDir, SourcePrefixToStrip: logsDir},
342 },
343 OutputFilePath: filepath.Join(config.DistDir(), "logs.zip"),
344 NumParallelJobs: runtime.NumCPU(),
345 CompressionLevel: 5,
346 }
Colin Cross05518bc2018-09-27 15:06:19 -0700347 if err := zip.Zip(args); err != nil {
Dan Willemsene3480762017-11-07 11:23:27 -0800348 log.Fatalf("Error zipping logs: %v", err)
349 }
350 }
351
Dan Willemsenb82471a2018-05-17 16:37:09 -0700352 s.Finish()
Dan Willemsen34218ec2018-07-19 22:57:18 -0700353
354 if failures == 1 {
355 log.Fatal("1 failure")
356 } else if failures > 1 {
357 log.Fatalf("%d failures", failures)
358 } else {
Colin Crosse0df1a32019-06-09 19:40:08 -0700359 fmt.Fprintln(output, "Success")
Dan Willemsen34218ec2018-07-19 22:57:18 -0700360 }
Dan Willemsenc2af0be2017-01-20 14:10:01 -0800361}
Dan Willemsen34218ec2018-07-19 22:57:18 -0700362
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700363func buildProduct(mpctx *mpContext, product string) {
364 var stdLog string
365
366 outDir := filepath.Join(mpctx.Config.OutDir(), product)
367 logsDir := filepath.Join(mpctx.LogsDir, product)
368
369 if err := os.MkdirAll(outDir, 0777); err != nil {
370 mpctx.Logger.Fatalf("Error creating out directory: %v", err)
371 }
372 if err := os.MkdirAll(logsDir, 0777); err != nil {
373 mpctx.Logger.Fatalf("Error creating log directory: %v", err)
374 }
375
376 stdLog = filepath.Join(logsDir, "std.log")
377 f, err := os.Create(stdLog)
378 if err != nil {
379 mpctx.Logger.Fatalf("Error creating std.log: %v", err)
380 }
381 defer f.Close()
382
383 log := logger.New(f)
384 defer log.Cleanup()
385 log.SetOutput(filepath.Join(logsDir, "soong.log"))
386
387 action := &status.Action{
388 Description: product,
389 Outputs: []string{product},
390 }
391 mpctx.Status.StartAction(action)
392 defer logger.Recover(func(err error) {
393 mpctx.Status.FinishAction(status.ActionResult{
394 Action: action,
395 Error: err,
396 Output: errMsgFromLog(stdLog),
397 })
398 })
399
400 ctx := build.Context{ContextImpl: &build.ContextImpl{
401 Context: mpctx.Context,
402 Logger: log,
403 Tracer: mpctx.Tracer,
Colin Cross097ed2a2019-06-08 21:48:58 -0700404 Writer: f,
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700405 Thread: mpctx.Tracer.NewThread(product),
406 Status: &status.Status{},
407 }}
Colin Crossc0b9f6b2019-09-23 12:44:54 -0700408 ctx.Status.AddOutput(terminal.NewStatusOutput(ctx.Writer, "", false,
Sasha Smundakc0c9ef92019-01-23 09:52:57 -0800409 build.OsEnvironment().IsEnvTrue("ANDROID_QUIET_BUILD")))
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700410
411 config := build.NewConfig(ctx, flag.Args()...)
412 config.Environment().Set("OUT_DIR", outDir)
Dan Willemsenf99915f2018-10-25 22:04:42 -0700413 if !*keepArtifacts {
414 config.Environment().Set("EMPTY_NINJA_FILE", "true")
415 }
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700416 build.FindSources(ctx, config, mpctx.Finder)
417 config.Lunch(ctx, product, *buildVariant)
418
419 defer func() {
420 if *keepArtifacts {
421 args := zip.ZipArgs{
422 FileArgs: []zip.FileArg{
423 {
424 GlobDir: outDir,
425 SourcePrefixToStrip: outDir,
426 },
427 },
428 OutputFilePath: filepath.Join(mpctx.Config.OutDir(), product+".zip"),
429 NumParallelJobs: runtime.NumCPU(),
430 CompressionLevel: 5,
431 }
Colin Cross05518bc2018-09-27 15:06:19 -0700432 if err := zip.Zip(args); err != nil {
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700433 log.Fatalf("Error zipping artifacts: %v", err)
434 }
435 }
Dan Willemsenf99915f2018-10-25 22:04:42 -0700436 if !*incremental {
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700437 os.RemoveAll(outDir)
438 }
439 }()
440
441 buildWhat := build.BuildProductConfig
442 if !*onlyConfig {
443 buildWhat |= build.BuildSoong
444 if !*onlySoong {
445 buildWhat |= build.BuildKati
446 }
447 }
448
449 before := time.Now()
450 build.Build(ctx, config, buildWhat)
451
452 // Save std_full.log if Kati re-read the makefiles
453 if buildWhat&build.BuildKati != 0 {
Dan Willemsen29971232018-09-26 14:58:30 -0700454 if after, err := os.Stat(config.KatiBuildNinjaFile()); err == nil && after.ModTime().After(before) {
Dan Willemsenbcc1dbf2018-09-04 18:09:47 -0700455 err := copyFile(stdLog, filepath.Join(filepath.Dir(stdLog), "std_full.log"))
456 if err != nil {
457 log.Fatalf("Error copying log file: %s", err)
458 }
459 }
460 }
461
462 mpctx.Status.FinishAction(status.ActionResult{
463 Action: action,
464 })
465}
466
Dan Willemsen34218ec2018-07-19 22:57:18 -0700467type failureCount int
468
469func (f *failureCount) StartAction(action *status.Action, counts status.Counts) {}
470
471func (f *failureCount) FinishAction(result status.ActionResult, counts status.Counts) {
472 if result.Error != nil {
473 *f += 1
474 }
475}
476
477func (f *failureCount) Message(level status.MsgLevel, message string) {
478 if level >= status.ErrorLvl {
479 *f += 1
480 }
481}
482
483func (f *failureCount) Flush() {}
Colin Crosse0df1a32019-06-09 19:40:08 -0700484
485func (f *failureCount) Write(p []byte) (int, error) {
486 // discard writes
487 return len(p), nil
488}
Dan Willemsen9609ad92019-12-05 15:22:41 -0800489
490func splitList(list []string, shardCount int) (ret [][]string) {
491 each := len(list) / shardCount
492 extra := len(list) % shardCount
493 for i := 0; i < shardCount; i++ {
494 count := each
495 if extra > 0 {
496 count += 1
497 extra -= 1
498 }
499 ret = append(ret, list[:count])
500 list = list[count:]
501 }
502 return
503}