blob: 83e34c2933c9105f3062781be307f12ef361b69f [file]
package main
import (
"bytes"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"android/soong/response"
"android/soong/third_party/zip"
"github.com/google/blueprint/pathtools"
)
var (
rspFile = flag.String("i", "", "RSP file containing a whitespace-separated list of input ZIPs")
filter multiFlag
)
func init() {
flag.Var(&filter, "f", "optional filter pattern")
}
type zipReader struct {
reader *zip.Reader
zipPath string
}
type zipWriter struct {
writer *zip.Writer
zipBytes *bytes.Buffer
outputPath string
}
func main() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: split_zips -i input.rsp -f \"glob\" output1.zip [output2.zip ...]")
flag.PrintDefaults()
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "Splits the combined content of input zips (from RSP file) into the specified output zip files.")
fmt.Fprintln(os.Stderr, "<-f> uses the rules at https://godoc.org/github.com/google/blueprint/pathtools/#Match to filter content.")
fmt.Fprintln(os.Stderr, "Mod time of output zip is set same as that of the first entry in zip.")
fmt.Fprintln(os.Stderr, "Directory entries in input zips are skipped.")
fmt.Fprintln(os.Stderr, "Duplicate files in input zips are not allowed.")
}
flag.Parse()
outputZipPaths := flag.Args() // Get positional arguments for output zip paths
if *rspFile == "" {
log.Println("Error: Input RSP file (-i) is required.")
flag.Usage()
os.Exit(1)
}
if len(outputZipPaths) == 0 {
log.Println("Error: At least one output ZIP file path must be provided.")
flag.Usage()
os.Exit(1)
}
log.SetFlags(log.Lshortfile)
rsp, err := os.Open(*rspFile)
if err != nil {
fmt.Println("err: ", err)
panic(err)
}
inputZips, err := response.ReadRspFile(rsp)
if err != nil {
panicOnError(err)
}
if len(inputZips) == 0 {
panicOnError(fmt.Errorf("no input zips found"))
}
var zipReaders []zipReader
var zipWriters []zipWriter
for _, inputZip := range inputZips {
reader, err := zip.OpenReader(inputZip)
if err != nil {
panicOnError(fmt.Errorf("warning: Failed to open input ZIP %s: %v", inputZip, err))
}
zipReaders = append(zipReaders, zipReader{
reader: &reader.Reader,
zipPath: inputZip,
})
defer reader.Close()
}
for _, outputZipPath := range outputZipPaths {
outputBuf := &bytes.Buffer{}
zipWriters = append(zipWriters, zipWriter{
writer: zip.NewWriter(outputBuf),
zipBytes: outputBuf,
outputPath: outputZipPath,
})
}
processAndSplit(zipReaders, zipWriters)
// Write the outputs from buffer
for _, zipW := range zipWriters {
zipW.writer.Close()
writeIfChanged(zipW.outputPath, zipW.zipBytes)
}
}
func processAndSplit(readers []zipReader, writers []zipWriter) {
numSplits := len(writers)
var allFileEntries []*zip.File
filesSeenInAllZips := make(map[string]string)
for _, zipR := range readers {
for _, f := range zipR.reader.File {
if filter != nil {
if match, err := filter.Match(filepath.Base(f.Name)); err != nil {
panicOnError(err)
} else if !match {
continue
}
}
if _, seen := filesSeenInAllZips[f.Name]; !seen {
filesSeenInAllZips[f.Name] = zipR.zipPath
// Directories are skipped.
if !f.FileInfo().IsDir() {
allFileEntries = append(allFileEntries, f)
}
} else {
if !f.FileInfo().IsDir() {
panicOnError(fmt.Errorf("info: Duplicate file '%s' found in ZIP(s) '%s' and '%s'", f.Name, filesSeenInAllZips[f.Name], zipR.zipPath))
}
}
}
}
if len(allFileEntries) == 0 {
panicOnError(fmt.Errorf("no processable files found in input ZIPs"))
}
if numSplits > len(allFileEntries) {
panicOnError(fmt.Errorf("number of splits '%d' more than total files '%d' in input zip(s)", numSplits, len(allFileEntries)))
}
// Determine how many actual files go into each split (approximately)
filesPerSplitTarget := (len(allFileEntries) + numSplits - 1) / numSplits // Ceiling division
currentOutputIndex := 0
filesInCurrentSplit := 0
for _, f := range allFileEntries {
// If we reach max capacity for a split, use the next output
if filesInCurrentSplit >= filesPerSplitTarget && currentOutputIndex < numSplits {
currentOutputIndex++
filesInCurrentSplit = 0
}
// Get the current zip writer
currentWriter := writers[currentOutputIndex]
if currentWriter.writer == nil {
panicOnError(fmt.Errorf("zip Writer is nil"))
}
// f is from allFileEntries, its expected that its original reader is not
// closed at this point. If it is, error will be thrown.
err := currentWriter.writer.CopyFrom(f, f.Name)
if err != nil {
panicOnError(err)
}
filesInCurrentSplit++
}
}
type multiFlag []string
func (m *multiFlag) String() string {
return strings.Join(*m, " ")
}
func (m *multiFlag) Set(s string) error {
*m = append(*m, s)
return nil
}
func (m *multiFlag) Match(s string) (bool, error) {
if m == nil {
return false, nil
}
for _, f := range *m {
if match, err := filepath.Match(f, s); err != nil {
return false, err
} else if match {
return true, nil
}
}
return false, nil
}
func writeIfChanged(outputFile string, buffer *bytes.Buffer) {
if err := pathtools.WriteFileIfChanged(outputFile, buffer.Bytes(), 0666); err != nil {
panicOnError(err)
}
}
func panicOnError(err error) {
fmt.Println("err: ", err)
panic(err)
}