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