• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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