| // Copyright 2018 The Bazel Authors. 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 bucketize |
| |
| import ( |
| "archive/zip" |
| "bytes" |
| "encoding/xml" |
| "fmt" |
| "io" |
| "os" |
| "path" |
| "path/filepath" |
| "sort" |
| "strings" |
| |
| "src/common/golang/shard" |
| "src/common/golang/xml2" |
| "src/tools/ak/res/res" |
| ) |
| |
| // Helper struct to sort paths by index |
| type indexedPaths struct { |
| order map[string]int |
| ps []string |
| } |
| |
| type byPathIndex indexedPaths |
| |
| func (b byPathIndex) Len() int { return len(b.ps) } |
| func (b byPathIndex) Swap(i, j int) { b.ps[i], b.ps[j] = b.ps[j], b.ps[i] } |
| func (b byPathIndex) Less(i, j int) bool { |
| iIdx := pathIdx(b.ps[i], b.order) |
| jIdx := pathIdx(b.ps[j], b.order) |
| // Files exist in the same directory |
| if iIdx == jIdx { |
| return b.ps[i] < b.ps[j] |
| } |
| return iIdx < jIdx |
| } |
| |
| // Helper struct to sort valuesKeys by index |
| type indexedValuesKeys struct { |
| order map[string]int |
| ks []valuesKey |
| } |
| |
| type byValueKeyIndex indexedValuesKeys |
| |
| func (b byValueKeyIndex) Len() int { return len(b.ks) } |
| func (b byValueKeyIndex) Swap(i, j int) { b.ks[i], b.ks[j] = b.ks[j], b.ks[i] } |
| func (b byValueKeyIndex) Less(i, j int) bool { |
| iIdx := pathIdx(b.ks[i].sourcePath.Path, b.order) |
| jIdx := pathIdx(b.ks[j].sourcePath.Path, b.order) |
| // Files exist in the same directory |
| if iIdx == jIdx { |
| return b.ks[i].sourcePath.Path < b.ks[j].sourcePath.Path |
| } |
| return iIdx < jIdx |
| } |
| |
| type valuesKey struct { |
| sourcePath res.PathInfo |
| resType res.Type |
| } |
| |
| // PartitionSession consumes resources and partitions them into archives by the resource type. |
| // The typewise partitions can be further sharded by the provided shardFn |
| type PartitionSession struct { |
| typedOutput map[res.Type][]*zip.Writer |
| sharder shard.Func |
| collectedVals map[valuesKey]map[string][]byte |
| collectedPaths map[string]res.PathInfo |
| collectedRAs map[string][]xml.Attr |
| resourceOrder map[string]int |
| } |
| |
| // Partitioner takes the provided resource values and paths and stores the data sharded |
| type Partitioner interface { |
| Close() error |
| CollectValues(vr *res.ValuesResource) error |
| CollectPathResource(src res.PathInfo) |
| CollectResourcesAttribute(attr *ResourcesAttribute) |
| } |
| |
| // makePartitionSession creates a PartitionSession that writes to the given outputs. |
| func makePartitionSession(outputs map[res.Type][]io.Writer, sharder shard.Func, resourceOrder map[string]int) (*PartitionSession, error) { |
| typeToArchs := make(map[res.Type][]*zip.Writer) |
| for t, ws := range outputs { |
| archs := make([]*zip.Writer, 0, len(ws)) |
| for _, w := range ws { |
| archs = append(archs, zip.NewWriter(w)) |
| } |
| typeToArchs[t] = archs |
| } |
| return &PartitionSession{ |
| typeToArchs, |
| sharder, |
| make(map[valuesKey]map[string][]byte), |
| make(map[string]res.PathInfo), |
| make(map[string][]xml.Attr), |
| resourceOrder, |
| }, nil |
| } |
| |
| // Close finalizes all archives in this partition session. |
| func (ps *PartitionSession) Close() error { |
| if err := ps.flushCollectedPaths(); err != nil { |
| return fmt.Errorf("got error flushing collected paths: %v", err) |
| } |
| if err := ps.flushCollectedVals(); err != nil { |
| return fmt.Errorf("got error flushing collected values: %v", err) |
| } |
| // close archives. |
| for _, as := range ps.typedOutput { |
| for _, a := range as { |
| if err := a.Close(); err != nil { |
| return fmt.Errorf("%s: could not close: %v", a, err) |
| } |
| } |
| } |
| return nil |
| } |
| |
| // CollectPathResource takes a file system resource and tracks it so that it can be stored in an output partition and shard. |
| func (ps *PartitionSession) CollectPathResource(src res.PathInfo) { |
| // store the path only if the type is accepted by the underlying partitions. |
| if ps.isTypeAccepted(src.Type) { |
| ps.collectedPaths[src.Path] = src |
| } |
| } |
| |
| // CollectValues stores the xml representation of a particular resource from a particular file. |
| func (ps *PartitionSession) CollectValues(vr *res.ValuesResource) error { |
| // store the value only if the type is accepted by the underlying partitions. |
| if ps.isTypeAccepted(vr.N.Type) { |
| // Don't store style attr's from other packages |
| if !(vr.N.Type == res.Attr && vr.N.Package != "res-auto") { |
| k := valuesKey{*vr.Src, vr.N.Type} |
| if tv, ok := ps.collectedVals[k]; !ok { |
| ps.collectedVals[k] = make(map[string][]byte) |
| ps.collectedVals[k][vr.N.String()] = vr.Payload |
| } else { |
| if p, ok := tv[vr.N.String()]; !ok { |
| ps.collectedVals[k][vr.N.String()] = vr.Payload |
| } else if len(p) < len(vr.Payload) { |
| ps.collectedVals[k][vr.N.String()] = vr.Payload |
| } else if len(p) == len(vr.Payload) && bytes.Compare(p, vr.Payload) != 0 { |
| return fmt.Errorf("different values for resource %q", vr.N.String()) |
| } |
| } |
| } |
| } |
| return nil |
| } |
| |
| // CollectResourcesAttribute stores the xml attributes of the resources tag from a particular file. |
| func (ps *PartitionSession) CollectResourcesAttribute(ra *ResourcesAttribute) { |
| ps.collectedRAs[ra.ResFile.Path] = append(ps.collectedRAs[ra.ResFile.Path], ra.Attribute) |
| } |
| |
| func (ps *PartitionSession) isTypeAccepted(t res.Type) bool { |
| _, ok := ps.typedOutput[t] |
| return ok |
| } |
| |
| func (ps *PartitionSession) flushCollectedPaths() error { |
| // sort keys so that data is written to the archives in a deterministic order |
| // specifically the same order in which they were declared |
| ks := make([]string, 0, len(ps.collectedPaths)) |
| for k := range ps.collectedPaths { |
| ks = append(ks, k) |
| } |
| sort.Sort(byPathIndex(indexedPaths{order: ps.resourceOrder, ps: ks})) |
| for _, k := range ks { |
| v := ps.collectedPaths[k] |
| f, err := os.Open(v.Path) |
| if err != nil { |
| return fmt.Errorf("%s: could not be opened for reading: %v", v.Path, err) |
| } |
| if err := ps.storePathResource(v, f); err != nil { |
| return fmt.Errorf("%s: got error storing path resource: %v", v.Path, err) |
| } |
| f.Close() |
| } |
| return nil |
| } |
| |
| func (ps *PartitionSession) storePathResource(src res.PathInfo, r io.Reader) error { |
| p := path.Base(src.Path) |
| if dot := strings.Index(p, "."); dot == 0 { |
| // skip files where the name starts with a ".", these are already ignored by aapt |
| return nil |
| } else if dot > 0 { |
| p = p[:dot] |
| } |
| fqn, err := res.ParseName(p, src.Type) |
| if err != nil { |
| return fmt.Errorf("%s: %q could not be parsed into a res name: %v", src.Path, p, err) |
| } |
| arch, err := ps.archiveFor(fqn) |
| if err != nil { |
| return fmt.Errorf("%s: could not get partitioned archive: %v", src.Path, err) |
| } |
| w, err := arch.Create(pathResSuffix(src.Path)) |
| if err != nil { |
| return fmt.Errorf("%s: could not create writer: %v", src.Path, err) |
| } |
| if _, err = io.Copy(w, r); err != nil { |
| return fmt.Errorf("%s: could not copy into archive: %v", src.Path, err) |
| } |
| return nil |
| } |
| |
| func (ps *PartitionSession) archiveFor(fqn res.FullyQualifiedName) (*zip.Writer, error) { |
| archs, ok := ps.typedOutput[fqn.Type] |
| if !ok { |
| return nil, fmt.Errorf("%s: do not have output stream for this res type", fqn.Type) |
| } |
| shard := ps.sharder(fqn.String(), len(archs)) |
| if shard > len(archs) || 0 > shard { |
| return nil, fmt.Errorf("%v: bad sharder f(%v, %d) -> %d must be [0,%d)", ps.sharder, fqn, len(archs), shard, len(archs)) |
| } |
| return archs[shard], nil |
| } |
| |
| var ( |
| resXMLHeader = []byte("<?xml version='1.0' encoding='utf-8'?>") |
| resXMLFooter = []byte("</resources>") |
| ) |
| |
| func (ps *PartitionSession) flushCollectedVals() error { |
| // sort keys so that data is written to the archives in a deterministic order |
| // specifically the same order in which blaze provides them |
| ks := make([]valuesKey, 0, len(ps.collectedVals)) |
| for k := range ps.collectedVals { |
| ks = append(ks, k) |
| } |
| sort.Sort(byValueKeyIndex(indexedValuesKeys{order: ps.resourceOrder, ks: ks})) |
| for _, k := range ks { |
| as, ok := ps.typedOutput[k.resType] |
| if !ok { |
| return fmt.Errorf("%s: no output for res type", k.resType) |
| } |
| ws := make([]io.Writer, 0, len(as)) |
| // For each given source file, create a corresponding file in each of the shards. A file in a particular shard may be empty, if none of the resources defined in the source file ended up in that shard. |
| for _, a := range as { |
| w, err := a.Create(pathResSuffix(k.sourcePath.Path)) |
| if err != nil { |
| return fmt.Errorf("%s: could not create entry: %v", k.sourcePath.Path, err) |
| } |
| if _, err = w.Write(resXMLHeader); err != nil { |
| return fmt.Errorf("%s: could not write xml header: %v", k.sourcePath.Path, err) |
| } |
| // Write the resources open tag, with the attributes collected. |
| b := bytes.Buffer{} |
| xml2.NewEncoder(&b).EncodeToken(xml.StartElement{ |
| Name: res.ResourcesTagName, |
| Attr: ps.collectedRAs[k.sourcePath.Path], |
| }) |
| if _, err = w.Write(b.Bytes()); err != nil { |
| return fmt.Errorf("%s: could not write resources tag %q: %v", k.sourcePath.Path, b.String(), err) |
| } |
| ws = append(ws, w) |
| } |
| v := ps.collectedVals[k] |
| var keys []string |
| for k := range v { |
| keys = append(keys, k) |
| } |
| sort.Strings(keys) |
| for _, fqn := range keys { |
| p := v[fqn] |
| shard := ps.sharder(fqn, len(ws)) |
| if shard < 0 || shard >= len(ws) { |
| return fmt.Errorf("%v: bad sharder f(%s, %d) -> %d must be [0,%d)", ps.sharder, fqn, len(ws), shard, len(ws)) |
| } |
| if _, err := ws[shard].Write(p); err != nil { |
| return fmt.Errorf("%s: writing resource %s failed: %v", k.sourcePath.Path, fqn, err) |
| } |
| } |
| for _, w := range ws { |
| if _, err := w.Write(resXMLFooter); err != nil { |
| return fmt.Errorf("%s: could not write xml footer: %v", k.sourcePath.Path, err) |
| } |
| } |
| } |
| return nil |
| } |
| |
| func pathIdx(path string, order map[string]int) int { |
| if idx, ok := order[path]; ok == true { |
| return idx |
| } |
| // TODO(mauriciogg): maybe replace with prefix search |
| // list of resources might contain directories so exact match might not exist |
| dirPos := strings.LastIndex(path, "/res/") |
| idx, _ := order[path[0:dirPos+4]] |
| return idx |
| } |
| |
| func pathResSuffix(path string) string { |
| // returns the relative resource path from the full path |
| // e.g. /foo/bar/res/values/strings.xml -> res/values/strings.xml |
| parentDir := filepath.Dir(filepath.Dir(filepath.Dir(path))) |
| return strings.TrimPrefix(path, parentDir+string(filepath.Separator)) |
| } |