487 lines
12 KiB
Go
487 lines
12 KiB
Go
// Copyright 2017 Google Inc. All rights reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"hash/crc32"
|
|
"io"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"github.com/google/blueprint/pathtools"
|
|
|
|
"android/soong/jar"
|
|
"android/soong/third_party/zip"
|
|
)
|
|
|
|
type fileList []string
|
|
|
|
func (f *fileList) String() string {
|
|
return `""`
|
|
}
|
|
|
|
func (f *fileList) Set(name string) error {
|
|
*f = append(*f, filepath.Clean(name))
|
|
|
|
return nil
|
|
}
|
|
|
|
type zipsToNotStripSet map[string]bool
|
|
|
|
func (s zipsToNotStripSet) String() string {
|
|
return `""`
|
|
}
|
|
|
|
func (s zipsToNotStripSet) Set(zip_path string) error {
|
|
s[zip_path] = true
|
|
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
sortEntries = flag.Bool("s", false, "sort entries (defaults to the order from the input zip files)")
|
|
emulateJar = flag.Bool("j", false, "sort zip entries using jar ordering (META-INF first)")
|
|
emulatePar = flag.Bool("p", false, "merge zip entries based on par format")
|
|
stripDirs fileList
|
|
stripFiles fileList
|
|
zipsToNotStrip = make(zipsToNotStripSet)
|
|
stripDirEntries = flag.Bool("D", false, "strip directory entries from the output zip file")
|
|
manifest = flag.String("m", "", "manifest file to insert in jar")
|
|
pyMain = flag.String("pm", "", "__main__.py file to insert in par")
|
|
prefix = flag.String("prefix", "", "A file to prefix to the zip file")
|
|
ignoreDuplicates = flag.Bool("ignore-duplicates", false, "take each entry from the first zip it exists in and don't warn")
|
|
)
|
|
|
|
func init() {
|
|
flag.Var(&stripDirs, "stripDir", "directories to be excluded from the output zip, accepts wildcards")
|
|
flag.Var(&stripFiles, "stripFile", "files to be excluded from the output zip, accepts wildcards")
|
|
flag.Var(&zipsToNotStrip, "zipToNotStrip", "the input zip file which is not applicable for stripping")
|
|
}
|
|
|
|
func main() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintln(os.Stderr, "usage: merge_zips [-jpsD] [-m manifest] [--prefix script] [-pm __main__.py] output [inputs...]")
|
|
flag.PrintDefaults()
|
|
}
|
|
|
|
// parse args
|
|
flag.Parse()
|
|
args := flag.Args()
|
|
if len(args) < 1 {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
outputPath := args[0]
|
|
inputs := args[1:]
|
|
|
|
log.SetFlags(log.Lshortfile)
|
|
|
|
// make writer
|
|
output, err := os.Create(outputPath)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer output.Close()
|
|
|
|
var offset int64
|
|
if *prefix != "" {
|
|
prefixFile, err := os.Open(*prefix)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
offset, err = io.Copy(output, prefixFile)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
writer := zip.NewWriter(output)
|
|
defer func() {
|
|
err := writer.Close()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}()
|
|
writer.SetOffset(offset)
|
|
|
|
// make readers
|
|
readers := []namedZipReader{}
|
|
for _, input := range inputs {
|
|
reader, err := zip.OpenReader(input)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
defer reader.Close()
|
|
namedReader := namedZipReader{path: input, reader: &reader.Reader}
|
|
readers = append(readers, namedReader)
|
|
}
|
|
|
|
if *manifest != "" && !*emulateJar {
|
|
log.Fatal(errors.New("must specify -j when specifying a manifest via -m"))
|
|
}
|
|
|
|
if *pyMain != "" && !*emulatePar {
|
|
log.Fatal(errors.New("must specify -p when specifying a Python __main__.py via -pm"))
|
|
}
|
|
|
|
// do merge
|
|
err = mergeZips(readers, writer, *manifest, *pyMain, *sortEntries, *emulateJar, *emulatePar,
|
|
*stripDirEntries, *ignoreDuplicates, []string(stripFiles), []string(stripDirs), map[string]bool(zipsToNotStrip))
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// a namedZipReader reads a .zip file and can say which file it's reading
|
|
type namedZipReader struct {
|
|
path string
|
|
reader *zip.Reader
|
|
}
|
|
|
|
// a zipEntryPath refers to a file contained in a zip
|
|
type zipEntryPath struct {
|
|
zipName string
|
|
entryName string
|
|
}
|
|
|
|
func (p zipEntryPath) String() string {
|
|
return p.zipName + "/" + p.entryName
|
|
}
|
|
|
|
// a zipEntry is a zipSource that pulls its content from another zip
|
|
type zipEntry struct {
|
|
path zipEntryPath
|
|
content *zip.File
|
|
}
|
|
|
|
func (ze zipEntry) String() string {
|
|
return ze.path.String()
|
|
}
|
|
|
|
func (ze zipEntry) IsDir() bool {
|
|
return ze.content.FileInfo().IsDir()
|
|
}
|
|
|
|
func (ze zipEntry) CRC32() uint32 {
|
|
return ze.content.FileHeader.CRC32
|
|
}
|
|
|
|
func (ze zipEntry) Size() uint64 {
|
|
return ze.content.FileHeader.UncompressedSize64
|
|
}
|
|
|
|
func (ze zipEntry) WriteToZip(dest string, zw *zip.Writer) error {
|
|
return zw.CopyFrom(ze.content, dest)
|
|
}
|
|
|
|
// a bufferEntry is a zipSource that pulls its content from a []byte
|
|
type bufferEntry struct {
|
|
fh *zip.FileHeader
|
|
content []byte
|
|
}
|
|
|
|
func (be bufferEntry) String() string {
|
|
return "internal buffer"
|
|
}
|
|
|
|
func (be bufferEntry) IsDir() bool {
|
|
return be.fh.FileInfo().IsDir()
|
|
}
|
|
|
|
func (be bufferEntry) CRC32() uint32 {
|
|
return crc32.ChecksumIEEE(be.content)
|
|
}
|
|
|
|
func (be bufferEntry) Size() uint64 {
|
|
return uint64(len(be.content))
|
|
}
|
|
|
|
func (be bufferEntry) WriteToZip(dest string, zw *zip.Writer) error {
|
|
w, err := zw.CreateHeader(be.fh)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !be.IsDir() {
|
|
_, err = w.Write(be.content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type zipSource interface {
|
|
String() string
|
|
IsDir() bool
|
|
CRC32() uint32
|
|
Size() uint64
|
|
WriteToZip(dest string, zw *zip.Writer) error
|
|
}
|
|
|
|
// a fileMapping specifies to copy a zip entry from one place to another
|
|
type fileMapping struct {
|
|
dest string
|
|
source zipSource
|
|
}
|
|
|
|
func mergeZips(readers []namedZipReader, writer *zip.Writer, manifest, pyMain string,
|
|
sortEntries, emulateJar, emulatePar, stripDirEntries, ignoreDuplicates bool,
|
|
stripFiles, stripDirs []string, zipsToNotStrip map[string]bool) error {
|
|
|
|
sourceByDest := make(map[string]zipSource, 0)
|
|
orderedMappings := []fileMapping{}
|
|
|
|
// if dest already exists returns a non-null zipSource for the existing source
|
|
addMapping := func(dest string, source zipSource) zipSource {
|
|
mapKey := filepath.Clean(dest)
|
|
if existingSource, exists := sourceByDest[mapKey]; exists {
|
|
return existingSource
|
|
}
|
|
|
|
sourceByDest[mapKey] = source
|
|
orderedMappings = append(orderedMappings, fileMapping{source: source, dest: dest})
|
|
return nil
|
|
}
|
|
|
|
if manifest != "" {
|
|
if !stripDirEntries {
|
|
dirHeader := jar.MetaDirFileHeader()
|
|
dirSource := bufferEntry{dirHeader, nil}
|
|
addMapping(jar.MetaDir, dirSource)
|
|
}
|
|
|
|
contents, err := ioutil.ReadFile(manifest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fh, buf, err := jar.ManifestFileContents(contents)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fileSource := bufferEntry{fh, buf}
|
|
addMapping(jar.ManifestFile, fileSource)
|
|
}
|
|
|
|
if pyMain != "" {
|
|
buf, err := ioutil.ReadFile(pyMain)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fh := &zip.FileHeader{
|
|
Name: "__main__.py",
|
|
Method: zip.Store,
|
|
UncompressedSize64: uint64(len(buf)),
|
|
}
|
|
fh.SetMode(0700)
|
|
fh.SetModTime(jar.DefaultTime)
|
|
fileSource := bufferEntry{fh, buf}
|
|
addMapping("__main__.py", fileSource)
|
|
}
|
|
|
|
if emulatePar {
|
|
// the runfiles packages needs to be populated with "__init__.py".
|
|
newPyPkgs := []string{}
|
|
// the runfiles dirs have been treated as packages.
|
|
existingPyPkgSet := make(map[string]bool)
|
|
// put existing __init__.py files to a set first. This set is used for preventing
|
|
// generated __init__.py files from overwriting existing ones.
|
|
for _, namedReader := range readers {
|
|
for _, file := range namedReader.reader.File {
|
|
if filepath.Base(file.Name) != "__init__.py" {
|
|
continue
|
|
}
|
|
pyPkg := pathBeforeLastSlash(file.Name)
|
|
if _, found := existingPyPkgSet[pyPkg]; found {
|
|
panic(fmt.Errorf("found __init__.py path duplicates during pars merging: %q.", file.Name))
|
|
} else {
|
|
existingPyPkgSet[pyPkg] = true
|
|
}
|
|
}
|
|
}
|
|
for _, namedReader := range readers {
|
|
for _, file := range namedReader.reader.File {
|
|
var parentPath string /* the path after trimming last "/" */
|
|
if filepath.Base(file.Name) == "__init__.py" {
|
|
// for existing __init__.py files, we should trim last "/" for twice.
|
|
// eg. a/b/c/__init__.py ---> a/b
|
|
parentPath = pathBeforeLastSlash(pathBeforeLastSlash(file.Name))
|
|
} else {
|
|
parentPath = pathBeforeLastSlash(file.Name)
|
|
}
|
|
populateNewPyPkgs(parentPath, existingPyPkgSet, &newPyPkgs)
|
|
}
|
|
}
|
|
for _, pkg := range newPyPkgs {
|
|
var emptyBuf []byte
|
|
fh := &zip.FileHeader{
|
|
Name: filepath.Join(pkg, "__init__.py"),
|
|
Method: zip.Store,
|
|
UncompressedSize64: uint64(len(emptyBuf)),
|
|
}
|
|
fh.SetMode(0700)
|
|
fh.SetModTime(jar.DefaultTime)
|
|
fileSource := bufferEntry{fh, emptyBuf}
|
|
addMapping(filepath.Join(pkg, "__init__.py"), fileSource)
|
|
}
|
|
}
|
|
for _, namedReader := range readers {
|
|
_, skipStripThisZip := zipsToNotStrip[namedReader.path]
|
|
for _, file := range namedReader.reader.File {
|
|
if !skipStripThisZip {
|
|
if skip, err := shouldStripEntry(emulateJar, stripFiles, stripDirs, file.Name); err != nil {
|
|
return err
|
|
} else if skip {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if stripDirEntries && file.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
// check for other files or directories destined for the same path
|
|
dest := file.Name
|
|
|
|
// make a new entry to add
|
|
source := zipEntry{path: zipEntryPath{zipName: namedReader.path, entryName: file.Name}, content: file}
|
|
|
|
if existingSource := addMapping(dest, source); existingSource != nil {
|
|
// handle duplicates
|
|
if existingSource.IsDir() != source.IsDir() {
|
|
return fmt.Errorf("Directory/file mismatch at %v from %v and %v\n",
|
|
dest, existingSource, source)
|
|
}
|
|
|
|
if ignoreDuplicates {
|
|
continue
|
|
}
|
|
|
|
if emulateJar &&
|
|
file.Name == jar.ManifestFile || file.Name == jar.ModuleInfoClass {
|
|
// Skip manifest and module info files that are not from the first input file
|
|
continue
|
|
}
|
|
|
|
if source.IsDir() {
|
|
continue
|
|
}
|
|
|
|
if existingSource.CRC32() == source.CRC32() && existingSource.Size() == source.Size() {
|
|
continue
|
|
}
|
|
|
|
return fmt.Errorf("Duplicate path %v found in %v and %v\n",
|
|
dest, existingSource, source)
|
|
}
|
|
}
|
|
}
|
|
|
|
if emulateJar {
|
|
jarSort(orderedMappings)
|
|
} else if sortEntries {
|
|
alphanumericSort(orderedMappings)
|
|
}
|
|
|
|
for _, entry := range orderedMappings {
|
|
if err := entry.source.WriteToZip(entry.dest, writer); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Sets the given directory and all its ancestor directories as Python packages.
|
|
func populateNewPyPkgs(pkgPath string, existingPyPkgSet map[string]bool, newPyPkgs *[]string) {
|
|
for pkgPath != "" {
|
|
if _, found := existingPyPkgSet[pkgPath]; !found {
|
|
existingPyPkgSet[pkgPath] = true
|
|
*newPyPkgs = append(*newPyPkgs, pkgPath)
|
|
// Gets its ancestor directory by trimming last slash.
|
|
pkgPath = pathBeforeLastSlash(pkgPath)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func pathBeforeLastSlash(path string) string {
|
|
ret := filepath.Dir(path)
|
|
// filepath.Dir("abc") -> "." and filepath.Dir("/abc") -> "/".
|
|
if ret == "." || ret == "/" {
|
|
return ""
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func shouldStripEntry(emulateJar bool, stripFiles, stripDirs []string, name string) (bool, error) {
|
|
for _, dir := range stripDirs {
|
|
dir = filepath.Clean(dir)
|
|
patterns := []string{
|
|
dir + "/", // the directory itself
|
|
dir + "/**/*", // files recursively in the directory
|
|
dir + "/**/*/", // directories recursively in the directory
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
match, err := pathtools.Match(pattern, name)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s: %s", err.Error(), pattern)
|
|
} else if match {
|
|
if emulateJar {
|
|
// When merging jar files, don't strip META-INF/MANIFEST.MF even if stripping META-INF is
|
|
// requested.
|
|
// TODO(ccross): which files does this affect?
|
|
if name != jar.MetaDir && name != jar.ManifestFile {
|
|
return true, nil
|
|
}
|
|
}
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, pattern := range stripFiles {
|
|
if match, err := pathtools.Match(pattern, name); err != nil {
|
|
return false, fmt.Errorf("%s: %s", err.Error(), pattern)
|
|
} else if match {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func jarSort(files []fileMapping) {
|
|
sort.SliceStable(files, func(i, j int) bool {
|
|
return jar.EntryNamesLess(files[i].dest, files[j].dest)
|
|
})
|
|
}
|
|
|
|
func alphanumericSort(files []fileMapping) {
|
|
sort.SliceStable(files, func(i, j int) bool {
|
|
return files[i].dest < files[j].dest
|
|
})
|
|
}
|