1// Copyright 2018 The Bazel Authors. 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 bucketize 16 17import ( 18 "archive/zip" 19 "bytes" 20 "encoding/xml" 21 "fmt" 22 "io" 23 "os" 24 "path" 25 "path/filepath" 26 "sort" 27 "strings" 28 29 "src/common/golang/shard" 30 "src/common/golang/xml2" 31 "src/tools/ak/res/res" 32) 33 34// Helper struct to sort paths by index 35type indexedPaths struct { 36 order map[string]int 37 ps []string 38} 39 40type byPathIndex indexedPaths 41 42func (b byPathIndex) Len() int { return len(b.ps) } 43func (b byPathIndex) Swap(i, j int) { b.ps[i], b.ps[j] = b.ps[j], b.ps[i] } 44func (b byPathIndex) Less(i, j int) bool { 45 iIdx := pathIdx(b.ps[i], b.order) 46 jIdx := pathIdx(b.ps[j], b.order) 47 // Files exist in the same directory 48 if iIdx == jIdx { 49 return b.ps[i] < b.ps[j] 50 } 51 return iIdx < jIdx 52} 53 54// Helper struct to sort valuesKeys by index 55type indexedValuesKeys struct { 56 order map[string]int 57 ks []valuesKey 58} 59 60type byValueKeyIndex indexedValuesKeys 61 62func (b byValueKeyIndex) Len() int { return len(b.ks) } 63func (b byValueKeyIndex) Swap(i, j int) { b.ks[i], b.ks[j] = b.ks[j], b.ks[i] } 64func (b byValueKeyIndex) Less(i, j int) bool { 65 iIdx := pathIdx(b.ks[i].sourcePath.Path, b.order) 66 jIdx := pathIdx(b.ks[j].sourcePath.Path, b.order) 67 // Files exist in the same directory 68 if iIdx == jIdx { 69 return b.ks[i].sourcePath.Path < b.ks[j].sourcePath.Path 70 } 71 return iIdx < jIdx 72} 73 74type valuesKey struct { 75 sourcePath res.PathInfo 76 resType res.Type 77} 78 79// PartitionSession consumes resources and partitions them into archives by the resource type. 80// The typewise partitions can be further sharded by the provided shardFn 81type PartitionSession struct { 82 typedOutput map[res.Type][]*zip.Writer 83 sharder shard.Func 84 collectedVals map[valuesKey]map[string][]byte 85 collectedPaths map[string]res.PathInfo 86 collectedRAs map[string][]xml.Attr 87 resourceOrder map[string]int 88} 89 90// Partitioner takes the provided resource values and paths and stores the data sharded 91type Partitioner interface { 92 Close() error 93 CollectValues(vr *res.ValuesResource) error 94 CollectPathResource(src res.PathInfo) 95 CollectResourcesAttribute(attr *ResourcesAttribute) 96} 97 98// makePartitionSession creates a PartitionSession that writes to the given outputs. 99func makePartitionSession(outputs map[res.Type][]io.Writer, sharder shard.Func, resourceOrder map[string]int) (*PartitionSession, error) { 100 typeToArchs := make(map[res.Type][]*zip.Writer) 101 for t, ws := range outputs { 102 archs := make([]*zip.Writer, 0, len(ws)) 103 for _, w := range ws { 104 archs = append(archs, zip.NewWriter(w)) 105 } 106 typeToArchs[t] = archs 107 } 108 return &PartitionSession{ 109 typeToArchs, 110 sharder, 111 make(map[valuesKey]map[string][]byte), 112 make(map[string]res.PathInfo), 113 make(map[string][]xml.Attr), 114 resourceOrder, 115 }, nil 116} 117 118// Close finalizes all archives in this partition session. 119func (ps *PartitionSession) Close() error { 120 if err := ps.flushCollectedPaths(); err != nil { 121 return fmt.Errorf("got error flushing collected paths: %v", err) 122 } 123 if err := ps.flushCollectedVals(); err != nil { 124 return fmt.Errorf("got error flushing collected values: %v", err) 125 } 126 // close archives. 127 for _, as := range ps.typedOutput { 128 for _, a := range as { 129 if err := a.Close(); err != nil { 130 return fmt.Errorf("%s: could not close: %v", a, err) 131 } 132 } 133 } 134 return nil 135} 136 137// CollectPathResource takes a file system resource and tracks it so that it can be stored in an output partition and shard. 138func (ps *PartitionSession) CollectPathResource(src res.PathInfo) { 139 // store the path only if the type is accepted by the underlying partitions. 140 if ps.isTypeAccepted(src.Type) { 141 ps.collectedPaths[src.Path] = src 142 } 143} 144 145// CollectValues stores the xml representation of a particular resource from a particular file. 146func (ps *PartitionSession) CollectValues(vr *res.ValuesResource) error { 147 // store the value only if the type is accepted by the underlying partitions. 148 if ps.isTypeAccepted(vr.N.Type) { 149 // Don't store style attr's from other packages 150 if !(vr.N.Type == res.Attr && vr.N.Package != "res-auto") { 151 k := valuesKey{*vr.Src, vr.N.Type} 152 if tv, ok := ps.collectedVals[k]; !ok { 153 ps.collectedVals[k] = make(map[string][]byte) 154 ps.collectedVals[k][vr.N.String()] = vr.Payload 155 } else { 156 if p, ok := tv[vr.N.String()]; !ok { 157 ps.collectedVals[k][vr.N.String()] = vr.Payload 158 } else if len(p) < len(vr.Payload) { 159 ps.collectedVals[k][vr.N.String()] = vr.Payload 160 } else if len(p) == len(vr.Payload) && bytes.Compare(p, vr.Payload) != 0 { 161 return fmt.Errorf("different values for resource %q", vr.N.String()) 162 } 163 } 164 } 165 } 166 return nil 167} 168 169// CollectResourcesAttribute stores the xml attributes of the resources tag from a particular file. 170func (ps *PartitionSession) CollectResourcesAttribute(ra *ResourcesAttribute) { 171 ps.collectedRAs[ra.ResFile.Path] = append(ps.collectedRAs[ra.ResFile.Path], ra.Attribute) 172} 173 174func (ps *PartitionSession) isTypeAccepted(t res.Type) bool { 175 _, ok := ps.typedOutput[t] 176 return ok 177} 178 179func (ps *PartitionSession) flushCollectedPaths() error { 180 // sort keys so that data is written to the archives in a deterministic order 181 // specifically the same order in which they were declared 182 ks := make([]string, 0, len(ps.collectedPaths)) 183 for k := range ps.collectedPaths { 184 ks = append(ks, k) 185 } 186 sort.Sort(byPathIndex(indexedPaths{order: ps.resourceOrder, ps: ks})) 187 for _, k := range ks { 188 v := ps.collectedPaths[k] 189 f, err := os.Open(v.Path) 190 if err != nil { 191 return fmt.Errorf("%s: could not be opened for reading: %v", v.Path, err) 192 } 193 if err := ps.storePathResource(v, f); err != nil { 194 return fmt.Errorf("%s: got error storing path resource: %v", v.Path, err) 195 } 196 f.Close() 197 } 198 return nil 199} 200 201func (ps *PartitionSession) storePathResource(src res.PathInfo, r io.Reader) error { 202 p := path.Base(src.Path) 203 if dot := strings.Index(p, "."); dot == 0 { 204 // skip files where the name starts with a ".", these are already ignored by aapt 205 return nil 206 } else if dot > 0 { 207 p = p[:dot] 208 } 209 fqn, err := res.ParseName(p, src.Type) 210 if err != nil { 211 return fmt.Errorf("%s: %q could not be parsed into a res name: %v", src.Path, p, err) 212 } 213 arch, err := ps.archiveFor(fqn) 214 if err != nil { 215 return fmt.Errorf("%s: could not get partitioned archive: %v", src.Path, err) 216 } 217 w, err := arch.Create(pathResSuffix(src.Path)) 218 if err != nil { 219 return fmt.Errorf("%s: could not create writer: %v", src.Path, err) 220 } 221 if _, err = io.Copy(w, r); err != nil { 222 return fmt.Errorf("%s: could not copy into archive: %v", src.Path, err) 223 } 224 return nil 225} 226 227func (ps *PartitionSession) archiveFor(fqn res.FullyQualifiedName) (*zip.Writer, error) { 228 archs, ok := ps.typedOutput[fqn.Type] 229 if !ok { 230 return nil, fmt.Errorf("%s: do not have output stream for this res type", fqn.Type) 231 } 232 shard := ps.sharder(fqn.String(), len(archs)) 233 if shard > len(archs) || 0 > shard { 234 return nil, fmt.Errorf("%v: bad sharder f(%v, %d) -> %d must be [0,%d)", ps.sharder, fqn, len(archs), shard, len(archs)) 235 } 236 return archs[shard], nil 237} 238 239var ( 240 resXMLHeader = []byte("<?xml version='1.0' encoding='utf-8'?>") 241 resXMLFooter = []byte("</resources>") 242) 243 244func (ps *PartitionSession) flushCollectedVals() error { 245 // sort keys so that data is written to the archives in a deterministic order 246 // specifically the same order in which blaze provides them 247 ks := make([]valuesKey, 0, len(ps.collectedVals)) 248 for k := range ps.collectedVals { 249 ks = append(ks, k) 250 } 251 sort.Sort(byValueKeyIndex(indexedValuesKeys{order: ps.resourceOrder, ks: ks})) 252 for _, k := range ks { 253 as, ok := ps.typedOutput[k.resType] 254 if !ok { 255 return fmt.Errorf("%s: no output for res type", k.resType) 256 } 257 ws := make([]io.Writer, 0, len(as)) 258 // 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. 259 for _, a := range as { 260 w, err := a.Create(pathResSuffix(k.sourcePath.Path)) 261 if err != nil { 262 return fmt.Errorf("%s: could not create entry: %v", k.sourcePath.Path, err) 263 } 264 if _, err = w.Write(resXMLHeader); err != nil { 265 return fmt.Errorf("%s: could not write xml header: %v", k.sourcePath.Path, err) 266 } 267 // Write the resources open tag, with the attributes collected. 268 b := bytes.Buffer{} 269 xml2.NewEncoder(&b).EncodeToken(xml.StartElement{ 270 Name: res.ResourcesTagName, 271 Attr: ps.collectedRAs[k.sourcePath.Path], 272 }) 273 if _, err = w.Write(b.Bytes()); err != nil { 274 return fmt.Errorf("%s: could not write resources tag %q: %v", k.sourcePath.Path, b.String(), err) 275 } 276 ws = append(ws, w) 277 } 278 v := ps.collectedVals[k] 279 var keys []string 280 for k := range v { 281 keys = append(keys, k) 282 } 283 sort.Strings(keys) 284 for _, fqn := range keys { 285 p := v[fqn] 286 shard := ps.sharder(fqn, len(ws)) 287 if shard < 0 || shard >= len(ws) { 288 return fmt.Errorf("%v: bad sharder f(%s, %d) -> %d must be [0,%d)", ps.sharder, fqn, len(ws), shard, len(ws)) 289 } 290 if _, err := ws[shard].Write(p); err != nil { 291 return fmt.Errorf("%s: writing resource %s failed: %v", k.sourcePath.Path, fqn, err) 292 } 293 } 294 for _, w := range ws { 295 if _, err := w.Write(resXMLFooter); err != nil { 296 return fmt.Errorf("%s: could not write xml footer: %v", k.sourcePath.Path, err) 297 } 298 } 299 } 300 return nil 301} 302 303func pathIdx(path string, order map[string]int) int { 304 if idx, ok := order[path]; ok == true { 305 return idx 306 } 307 // TODO(mauriciogg): maybe replace with prefix search 308 // list of resources might contain directories so exact match might not exist 309 dirPos := strings.LastIndex(path, "/res/") 310 idx, _ := order[path[0:dirPos+4]] 311 return idx 312} 313 314func pathResSuffix(path string) string { 315 // returns the relative resource path from the full path 316 // e.g. /foo/bar/res/values/strings.xml -> res/values/strings.xml 317 parentDir := filepath.Dir(filepath.Dir(filepath.Dir(path))) 318 return strings.TrimPrefix(path, parentDir+string(filepath.Separator)) 319} 320