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 "errors" 19 "flag" 20 "fmt" 21 "hash/crc32" 22 "io" 23 "io/ioutil" 24 "log" 25 "os" 26 "path/filepath" 27 "sort" 28 "strings" 29 30 "android/soong/response" 31 32 "github.com/google/blueprint/pathtools" 33 34 "android/soong/jar" 35 "android/soong/third_party/zip" 36) 37 38// Input zip: we can open it, close it, and obtain an array of entries 39type InputZip interface { 40 Name() string 41 Open() error 42 Close() error 43 Entries() []*zip.File 44 IsOpen() bool 45} 46 47// An entry that can be written to the output zip 48type ZipEntryContents interface { 49 String() string 50 IsDir() bool 51 CRC32() uint32 52 Size() uint64 53 WriteToZip(dest string, zw *zip.Writer) error 54} 55 56// a ZipEntryFromZip is a ZipEntryContents that pulls its content from another zip 57// identified by the input zip and the index of the entry in its entries array 58type ZipEntryFromZip struct { 59 inputZip InputZip 60 index int 61 name string 62 isDir bool 63 crc32 uint32 64 size uint64 65} 66 67func NewZipEntryFromZip(inputZip InputZip, entryIndex int) *ZipEntryFromZip { 68 fi := inputZip.Entries()[entryIndex] 69 newEntry := ZipEntryFromZip{inputZip: inputZip, 70 index: entryIndex, 71 name: fi.Name, 72 isDir: fi.FileInfo().IsDir(), 73 crc32: fi.CRC32, 74 size: fi.UncompressedSize64, 75 } 76 return &newEntry 77} 78 79func (ze ZipEntryFromZip) String() string { 80 return fmt.Sprintf("%s!%s", ze.inputZip.Name(), ze.name) 81} 82 83func (ze ZipEntryFromZip) IsDir() bool { 84 return ze.isDir 85} 86 87func (ze ZipEntryFromZip) CRC32() uint32 { 88 return ze.crc32 89} 90 91func (ze ZipEntryFromZip) Size() uint64 { 92 return ze.size 93} 94 95func (ze ZipEntryFromZip) WriteToZip(dest string, zw *zip.Writer) error { 96 if err := ze.inputZip.Open(); err != nil { 97 return err 98 } 99 return zw.CopyFrom(ze.inputZip.Entries()[ze.index], dest) 100} 101 102// a ZipEntryFromBuffer is a ZipEntryContents that pulls its content from a []byte 103type ZipEntryFromBuffer struct { 104 fh *zip.FileHeader 105 content []byte 106} 107 108func (be ZipEntryFromBuffer) String() string { 109 return "internal buffer" 110} 111 112func (be ZipEntryFromBuffer) IsDir() bool { 113 return be.fh.FileInfo().IsDir() 114} 115 116func (be ZipEntryFromBuffer) CRC32() uint32 { 117 return crc32.ChecksumIEEE(be.content) 118} 119 120func (be ZipEntryFromBuffer) Size() uint64 { 121 return uint64(len(be.content)) 122} 123 124func (be ZipEntryFromBuffer) WriteToZip(dest string, zw *zip.Writer) error { 125 w, err := zw.CreateHeaderAndroid(be.fh) 126 if err != nil { 127 return err 128 } 129 130 if !be.IsDir() { 131 _, err = w.Write(be.content) 132 if err != nil { 133 return err 134 } 135 } 136 137 return nil 138} 139 140// Processing state. 141type OutputZip struct { 142 outputWriter *zip.Writer 143 stripDirEntries bool 144 emulateJar bool 145 sortEntries bool 146 ignoreDuplicates bool 147 excludeDirs []string 148 excludeFiles []string 149 sourceByDest map[string]ZipEntryContents 150} 151 152func NewOutputZip(outputWriter *zip.Writer, sortEntries, emulateJar, stripDirEntries, ignoreDuplicates bool) *OutputZip { 153 return &OutputZip{ 154 outputWriter: outputWriter, 155 stripDirEntries: stripDirEntries, 156 emulateJar: emulateJar, 157 sortEntries: sortEntries, 158 sourceByDest: make(map[string]ZipEntryContents, 0), 159 ignoreDuplicates: ignoreDuplicates, 160 } 161} 162 163func (oz *OutputZip) setExcludeDirs(excludeDirs []string) { 164 oz.excludeDirs = make([]string, len(excludeDirs)) 165 for i, dir := range excludeDirs { 166 oz.excludeDirs[i] = filepath.Clean(dir) 167 } 168} 169 170func (oz *OutputZip) setExcludeFiles(excludeFiles []string) { 171 oz.excludeFiles = excludeFiles 172} 173 174// Adds an entry with given name whose source is given ZipEntryContents. Returns old ZipEntryContents 175// if entry with given name already exists. 176func (oz *OutputZip) addZipEntry(name string, source ZipEntryContents) (ZipEntryContents, error) { 177 if existingSource, exists := oz.sourceByDest[name]; exists { 178 return existingSource, nil 179 } 180 oz.sourceByDest[name] = source 181 // Delay writing an entry if entries need to be rearranged. 182 if oz.emulateJar || oz.sortEntries { 183 return nil, nil 184 } 185 return nil, source.WriteToZip(name, oz.outputWriter) 186} 187 188// Adds an entry for the manifest (META-INF/MANIFEST.MF from the given file 189func (oz *OutputZip) addManifest(manifestPath string) error { 190 if !oz.stripDirEntries { 191 if _, err := oz.addZipEntry(jar.MetaDir, ZipEntryFromBuffer{jar.MetaDirFileHeader(), nil}); err != nil { 192 return err 193 } 194 } 195 contents, err := ioutil.ReadFile(manifestPath) 196 if err == nil { 197 fh, buf, err := jar.ManifestFileContents(contents) 198 if err == nil { 199 _, err = oz.addZipEntry(jar.ManifestFile, ZipEntryFromBuffer{fh, buf}) 200 } 201 } 202 return err 203} 204 205// Adds an entry with given name and contents read from given file 206func (oz *OutputZip) addZipEntryFromFile(name string, path string) error { 207 buf, err := ioutil.ReadFile(path) 208 if err == nil { 209 fh := &zip.FileHeader{ 210 Name: name, 211 Method: zip.Store, 212 UncompressedSize64: uint64(len(buf)), 213 } 214 fh.SetMode(0700) 215 fh.SetModTime(jar.DefaultTime) 216 _, err = oz.addZipEntry(name, ZipEntryFromBuffer{fh, buf}) 217 } 218 return err 219} 220 221func (oz *OutputZip) addEmptyEntry(entry string) error { 222 var emptyBuf []byte 223 fh := &zip.FileHeader{ 224 Name: entry, 225 Method: zip.Store, 226 UncompressedSize64: uint64(len(emptyBuf)), 227 } 228 fh.SetMode(0700) 229 fh.SetModTime(jar.DefaultTime) 230 _, err := oz.addZipEntry(entry, ZipEntryFromBuffer{fh, emptyBuf}) 231 return err 232} 233 234// Returns true if given entry is to be excluded 235func (oz *OutputZip) isEntryExcluded(name string) bool { 236 for _, dir := range oz.excludeDirs { 237 dir = filepath.Clean(dir) 238 patterns := []string{ 239 dir + "/", // the directory itself 240 dir + "/**/*", // files recursively in the directory 241 dir + "/**/*/", // directories recursively in the directory 242 } 243 244 for _, pattern := range patterns { 245 match, err := pathtools.Match(pattern, name) 246 if err != nil { 247 panic(fmt.Errorf("%s: %s", err.Error(), pattern)) 248 } 249 if match { 250 if oz.emulateJar { 251 // When merging jar files, don't strip META-INF/MANIFEST.MF even if stripping META-INF is 252 // requested. 253 // TODO(ccross): which files does this affect? 254 if name != jar.MetaDir && name != jar.ManifestFile { 255 return true 256 } 257 } 258 return true 259 } 260 } 261 } 262 263 for _, pattern := range oz.excludeFiles { 264 match, err := pathtools.Match(pattern, name) 265 if err != nil { 266 panic(fmt.Errorf("%s: %s", err.Error(), pattern)) 267 } 268 if match { 269 return true 270 } 271 } 272 return false 273} 274 275// Creates a zip entry whose contents is an entry from the given input zip. 276func (oz *OutputZip) copyEntry(inputZip InputZip, index int) error { 277 entry := NewZipEntryFromZip(inputZip, index) 278 if oz.stripDirEntries && entry.IsDir() { 279 return nil 280 } 281 existingEntry, err := oz.addZipEntry(entry.name, entry) 282 if err != nil { 283 return err 284 } 285 if existingEntry == nil { 286 return nil 287 } 288 289 // File types should match 290 if existingEntry.IsDir() != entry.IsDir() { 291 return fmt.Errorf("Directory/file mismatch at %v from %v and %v\n", 292 entry.name, existingEntry, entry) 293 } 294 295 if oz.ignoreDuplicates || 296 // Skip manifest and module info files that are not from the first input file 297 (oz.emulateJar && entry.name == jar.ManifestFile || entry.name == jar.ModuleInfoClass) || 298 // Identical entries 299 (existingEntry.CRC32() == entry.CRC32() && existingEntry.Size() == entry.Size()) || 300 // Directory entries 301 entry.IsDir() { 302 return nil 303 } 304 305 return fmt.Errorf("Duplicate path %v found in %v and %v\n", entry.name, existingEntry, inputZip.Name()) 306} 307 308func (oz *OutputZip) entriesArray() []string { 309 entries := make([]string, len(oz.sourceByDest)) 310 i := 0 311 for entry := range oz.sourceByDest { 312 entries[i] = entry 313 i++ 314 } 315 return entries 316} 317 318func (oz *OutputZip) jarSorted() []string { 319 entries := oz.entriesArray() 320 sort.SliceStable(entries, func(i, j int) bool { return jar.EntryNamesLess(entries[i], entries[j]) }) 321 return entries 322} 323 324func (oz *OutputZip) alphanumericSorted() []string { 325 entries := oz.entriesArray() 326 sort.Strings(entries) 327 return entries 328} 329 330func (oz *OutputZip) writeEntries(entries []string) error { 331 for _, entry := range entries { 332 source, _ := oz.sourceByDest[entry] 333 if err := source.WriteToZip(entry, oz.outputWriter); err != nil { 334 return err 335 } 336 } 337 return nil 338} 339 340func (oz *OutputZip) getUninitializedPythonPackages(inputZips []InputZip) ([]string, error) { 341 // the runfiles packages needs to be populated with "__init__.py". 342 // the runfiles dirs have been treated as packages. 343 allPackages := make(map[string]bool) 344 initedPackages := make(map[string]bool) 345 getPackage := func(path string) string { 346 ret := filepath.Dir(path) 347 // filepath.Dir("abc") -> "." and filepath.Dir("/abc") -> "/". 348 if ret == "." || ret == "/" { 349 return "" 350 } 351 return ret 352 } 353 354 // put existing __init__.py files to a set first. This set is used for preventing 355 // generated __init__.py files from overwriting existing ones. 356 for _, inputZip := range inputZips { 357 if err := inputZip.Open(); err != nil { 358 return nil, err 359 } 360 for _, file := range inputZip.Entries() { 361 pyPkg := getPackage(file.Name) 362 baseName := filepath.Base(file.Name) 363 if baseName == "__init__.py" || baseName == "__init__.pyc" { 364 if _, found := initedPackages[pyPkg]; found { 365 panic(fmt.Errorf("found __init__.py path duplicates during pars merging: %q", file.Name)) 366 } 367 initedPackages[pyPkg] = true 368 } 369 for pyPkg != "" { 370 if _, found := allPackages[pyPkg]; found { 371 break 372 } 373 allPackages[pyPkg] = true 374 pyPkg = getPackage(pyPkg) 375 } 376 } 377 } 378 noInitPackages := make([]string, 0) 379 for pyPkg := range allPackages { 380 if _, found := initedPackages[pyPkg]; !found { 381 noInitPackages = append(noInitPackages, pyPkg) 382 } 383 } 384 return noInitPackages, nil 385} 386 387// An InputZip owned by the InputZipsManager. Opened ManagedInputZip's are chained in the open order. 388type ManagedInputZip struct { 389 owner *InputZipsManager 390 realInputZip InputZip 391 older *ManagedInputZip 392 newer *ManagedInputZip 393} 394 395// Maintains the array of ManagedInputZips, keeping track of open input ones. When an InputZip is opened, 396// may close some other InputZip to limit the number of open ones. 397type InputZipsManager struct { 398 inputZips []*ManagedInputZip 399 nOpenZips int 400 maxOpenZips int 401 openInputZips *ManagedInputZip 402} 403 404func (miz *ManagedInputZip) unlink() { 405 olderMiz := miz.older 406 newerMiz := miz.newer 407 if newerMiz.older != miz || olderMiz.newer != miz { 408 panic(fmt.Errorf("removing %p:%#v: broken list between %p:%#v and %p:%#v", 409 miz, miz, newerMiz, newerMiz, olderMiz, olderMiz)) 410 } 411 olderMiz.newer = newerMiz 412 newerMiz.older = olderMiz 413 miz.newer = nil 414 miz.older = nil 415} 416 417func (miz *ManagedInputZip) link(olderMiz *ManagedInputZip) { 418 if olderMiz.newer != nil || olderMiz.older != nil { 419 panic(fmt.Errorf("inputZip is already open")) 420 } 421 oldOlderMiz := miz.older 422 if oldOlderMiz.newer != miz { 423 panic(fmt.Errorf("broken list between %p:%#v and %p:%#v", miz, miz, oldOlderMiz, oldOlderMiz)) 424 } 425 miz.older = olderMiz 426 olderMiz.older = oldOlderMiz 427 oldOlderMiz.newer = olderMiz 428 olderMiz.newer = miz 429} 430 431func NewInputZipsManager(nInputZips, maxOpenZips int) *InputZipsManager { 432 if maxOpenZips < 3 { 433 panic(fmt.Errorf("open zips limit should be above 3")) 434 } 435 // In the fake element .older points to the most recently opened InputZip, and .newer points to the oldest. 436 head := new(ManagedInputZip) 437 head.older = head 438 head.newer = head 439 return &InputZipsManager{ 440 inputZips: make([]*ManagedInputZip, 0, nInputZips), 441 maxOpenZips: maxOpenZips, 442 openInputZips: head, 443 } 444} 445 446// InputZip factory 447func (izm *InputZipsManager) Manage(inz InputZip) InputZip { 448 iz := &ManagedInputZip{owner: izm, realInputZip: inz} 449 izm.inputZips = append(izm.inputZips, iz) 450 return iz 451} 452 453// Opens or reopens ManagedInputZip. 454func (izm *InputZipsManager) reopen(miz *ManagedInputZip) error { 455 if miz.realInputZip.IsOpen() { 456 if miz != izm.openInputZips { 457 miz.unlink() 458 izm.openInputZips.link(miz) 459 } 460 return nil 461 } 462 if izm.nOpenZips >= izm.maxOpenZips { 463 if err := izm.close(izm.openInputZips.older); err != nil { 464 return err 465 } 466 } 467 if err := miz.realInputZip.Open(); err != nil { 468 return err 469 } 470 izm.openInputZips.link(miz) 471 izm.nOpenZips++ 472 return nil 473} 474 475func (izm *InputZipsManager) close(miz *ManagedInputZip) error { 476 if miz.IsOpen() { 477 err := miz.realInputZip.Close() 478 izm.nOpenZips-- 479 miz.unlink() 480 return err 481 } 482 return nil 483} 484 485// Checks that openInputZips deque is valid 486func (izm *InputZipsManager) checkOpenZipsDeque() { 487 nReallyOpen := 0 488 el := izm.openInputZips 489 for { 490 elNext := el.older 491 if elNext.newer != el { 492 panic(fmt.Errorf("Element:\n %p: %v\nNext:\n %p %v", el, el, elNext, elNext)) 493 } 494 if elNext == izm.openInputZips { 495 break 496 } 497 el = elNext 498 if !el.IsOpen() { 499 panic(fmt.Errorf("Found unopened element")) 500 } 501 nReallyOpen++ 502 if nReallyOpen > izm.nOpenZips { 503 panic(fmt.Errorf("found %d open zips, should be %d", nReallyOpen, izm.nOpenZips)) 504 } 505 } 506 if nReallyOpen > izm.nOpenZips { 507 panic(fmt.Errorf("found %d open zips, should be %d", nReallyOpen, izm.nOpenZips)) 508 } 509} 510 511func (miz *ManagedInputZip) Name() string { 512 return miz.realInputZip.Name() 513} 514 515func (miz *ManagedInputZip) Open() error { 516 return miz.owner.reopen(miz) 517} 518 519func (miz *ManagedInputZip) Close() error { 520 return miz.owner.close(miz) 521} 522 523func (miz *ManagedInputZip) IsOpen() bool { 524 return miz.realInputZip.IsOpen() 525} 526 527func (miz *ManagedInputZip) Entries() []*zip.File { 528 if !miz.IsOpen() { 529 panic(fmt.Errorf("%s: is not open", miz.Name())) 530 } 531 return miz.realInputZip.Entries() 532} 533 534// Actual processing. 535func mergeZips(inputZips []InputZip, writer *zip.Writer, manifest, pyMain string, 536 sortEntries, emulateJar, emulatePar, stripDirEntries, ignoreDuplicates bool, 537 excludeFiles, excludeDirs []string, zipsToNotStrip map[string]bool) error { 538 539 out := NewOutputZip(writer, sortEntries, emulateJar, stripDirEntries, ignoreDuplicates) 540 out.setExcludeFiles(excludeFiles) 541 out.setExcludeDirs(excludeDirs) 542 if manifest != "" { 543 if err := out.addManifest(manifest); err != nil { 544 return err 545 } 546 } 547 if pyMain != "" { 548 if err := out.addZipEntryFromFile("__main__.py", pyMain); err != nil { 549 return err 550 } 551 } 552 553 if emulatePar { 554 noInitPackages, err := out.getUninitializedPythonPackages(inputZips) 555 if err != nil { 556 return err 557 } 558 for _, uninitializedPyPackage := range noInitPackages { 559 if err = out.addEmptyEntry(filepath.Join(uninitializedPyPackage, "__init__.py")); err != nil { 560 return err 561 } 562 } 563 } 564 565 var jarServices jar.Services 566 567 // Finally, add entries from all the input zips. 568 for _, inputZip := range inputZips { 569 _, copyFully := zipsToNotStrip[inputZip.Name()] 570 if err := inputZip.Open(); err != nil { 571 return err 572 } 573 574 for i, entry := range inputZip.Entries() { 575 if emulateJar && jarServices.IsServiceFile(entry) { 576 // If this is a jar, collect service files to combine instead of adding them to the zip. 577 err := jarServices.AddServiceFile(entry) 578 if err != nil { 579 return err 580 } 581 continue 582 } 583 if copyFully || !out.isEntryExcluded(entry.Name) { 584 if err := out.copyEntry(inputZip, i); err != nil { 585 return err 586 } 587 } 588 } 589 // Unless we need to rearrange the entries, the input zip can now be closed. 590 if !(emulateJar || sortEntries) { 591 if err := inputZip.Close(); err != nil { 592 return err 593 } 594 } 595 } 596 597 if emulateJar { 598 // Combine all the service files into a single list of combined service files and add them to the zip. 599 for _, serviceFile := range jarServices.ServiceFiles() { 600 _, err := out.addZipEntry(serviceFile.Name, ZipEntryFromBuffer{ 601 fh: serviceFile.FileHeader, 602 content: serviceFile.Contents, 603 }) 604 if err != nil { 605 return err 606 } 607 } 608 return out.writeEntries(out.jarSorted()) 609 } else if sortEntries { 610 return out.writeEntries(out.alphanumericSorted()) 611 } 612 return nil 613} 614 615// Process command line 616type fileList []string 617 618func (f *fileList) String() string { 619 return `""` 620} 621 622func (f *fileList) Set(name string) error { 623 *f = append(*f, filepath.Clean(name)) 624 625 return nil 626} 627 628type zipsToNotStripSet map[string]bool 629 630func (s zipsToNotStripSet) String() string { 631 return `""` 632} 633 634func (s zipsToNotStripSet) Set(path string) error { 635 s[path] = true 636 return nil 637} 638 639var ( 640 sortEntries = flag.Bool("s", false, "sort entries (defaults to the order from the input zip files)") 641 emulateJar = flag.Bool("j", false, "sort zip entries using jar ordering (META-INF first)") 642 emulatePar = flag.Bool("p", false, "merge zip entries based on par format") 643 excludeDirs fileList 644 excludeFiles fileList 645 zipsToNotStrip = make(zipsToNotStripSet) 646 stripDirEntries = flag.Bool("D", false, "strip directory entries from the output zip file") 647 manifest = flag.String("m", "", "manifest file to insert in jar") 648 pyMain = flag.String("pm", "", "__main__.py file to insert in par") 649 prefix = flag.String("prefix", "", "A file to prefix to the zip file") 650 ignoreDuplicates = flag.Bool("ignore-duplicates", false, "take each entry from the first zip it exists in and don't warn") 651) 652 653func init() { 654 flag.Var(&excludeDirs, "stripDir", "directories to be excluded from the output zip, accepts wildcards") 655 flag.Var(&excludeFiles, "stripFile", "files to be excluded from the output zip, accepts wildcards") 656 flag.Var(&zipsToNotStrip, "zipToNotStrip", "the input zip file which is not applicable for stripping") 657} 658 659type FileInputZip struct { 660 name string 661 reader *zip.ReadCloser 662} 663 664func (fiz *FileInputZip) Name() string { 665 return fiz.name 666} 667 668func (fiz *FileInputZip) Close() error { 669 if fiz.IsOpen() { 670 reader := fiz.reader 671 fiz.reader = nil 672 return reader.Close() 673 } 674 return nil 675} 676 677func (fiz *FileInputZip) Entries() []*zip.File { 678 if !fiz.IsOpen() { 679 panic(fmt.Errorf("%s: is not open", fiz.Name())) 680 } 681 return fiz.reader.File 682} 683 684func (fiz *FileInputZip) IsOpen() bool { 685 return fiz.reader != nil 686} 687 688func (fiz *FileInputZip) Open() error { 689 if fiz.IsOpen() { 690 return nil 691 } 692 var err error 693 if fiz.reader, err = zip.OpenReader(fiz.Name()); err != nil { 694 return fmt.Errorf("%s: %s", fiz.Name(), err.Error()) 695 } 696 return nil 697} 698 699func main() { 700 flag.Usage = func() { 701 fmt.Fprintln(os.Stderr, "usage: merge_zips [-jpsD] [-m manifest] [--prefix script] [-pm __main__.py] OutputZip [inputs...]") 702 flag.PrintDefaults() 703 } 704 705 // parse args 706 flag.Parse() 707 args := flag.Args() 708 if len(args) < 1 { 709 flag.Usage() 710 os.Exit(1) 711 } 712 outputPath := args[0] 713 inputs := make([]string, 0) 714 for _, input := range args[1:] { 715 if input[0] == '@' { 716 f, err := os.Open(strings.TrimPrefix(input[1:], "@")) 717 if err != nil { 718 log.Fatal(err) 719 } 720 721 rspInputs, err := response.ReadRspFile(f) 722 f.Close() 723 if err != nil { 724 log.Fatal(err) 725 } 726 inputs = append(inputs, rspInputs...) 727 } else { 728 inputs = append(inputs, input) 729 } 730 } 731 732 log.SetFlags(log.Lshortfile) 733 734 // make writer 735 outputZip, err := os.Create(outputPath) 736 if err != nil { 737 log.Fatal(err) 738 } 739 defer outputZip.Close() 740 741 var offset int64 742 if *prefix != "" { 743 prefixFile, err := os.Open(*prefix) 744 if err != nil { 745 log.Fatal(err) 746 } 747 offset, err = io.Copy(outputZip, prefixFile) 748 if err != nil { 749 log.Fatal(err) 750 } 751 } 752 753 writer := zip.NewWriter(outputZip) 754 defer func() { 755 err := writer.Close() 756 if err != nil { 757 log.Fatal(err) 758 } 759 }() 760 writer.SetOffset(offset) 761 762 if *manifest != "" && !*emulateJar { 763 log.Fatal(errors.New("must specify -j when specifying a manifest via -m")) 764 } 765 766 if *pyMain != "" && !*emulatePar { 767 log.Fatal(errors.New("must specify -p when specifying a Python __main__.py via -pm")) 768 } 769 770 // do merge 771 inputZipsManager := NewInputZipsManager(len(inputs), 1000) 772 inputZips := make([]InputZip, len(inputs)) 773 for i, input := range inputs { 774 inputZips[i] = inputZipsManager.Manage(&FileInputZip{name: input}) 775 } 776 err = mergeZips(inputZips, writer, *manifest, *pyMain, *sortEntries, *emulateJar, *emulatePar, 777 *stripDirEntries, *ignoreDuplicates, []string(excludeFiles), []string(excludeDirs), 778 map[string]bool(zipsToNotStrip)) 779 if err != nil { 780 log.Fatal(err) 781 } 782} 783