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 15// Package finalrjar generates a valid final R.jar. 16package finalrjar 17 18import ( 19 "archive/zip" 20 "bufio" 21 "flag" 22 "fmt" 23 "io" 24 "log" 25 "os" 26 "os/exec" 27 "path/filepath" 28 "sort" 29 "strings" 30 "sync" 31 32 "src/common/golang/ziputils" 33 "src/tools/ak/types" 34) 35 36var ( 37 // Cmd defines the command. 38 Cmd = types.Command{ 39 Init: Init, 40 Run: Run, 41 Desc: desc, 42 Flags: []string{"package", "r_txts", "out_r_java", "root_pkg", "jdk", "jartool", "target_label"}, 43 } 44 45 // Variables to hold flag values. 46 pkg string 47 rtxts string 48 outputRJar string 49 rootPackage string 50 jdk string 51 jartool string 52 targetLabel string 53 54 initOnce sync.Once 55 56 resTypes = []string{ 57 "anim", 58 "animator", 59 "array", 60 "attr", 61 "^attr-private", 62 "bool", 63 "color", 64 "configVarying", 65 "dimen", 66 "drawable", 67 "fraction", 68 "font", 69 "id", 70 "integer", 71 "interpolator", 72 "layout", 73 "menu", 74 "mipmap", 75 "navigation", 76 "plurals", 77 "raw", 78 "string", 79 "style", 80 "styleable", 81 "transition", 82 "xml", 83 } 84 85 javaReserved = map[string]bool{ 86 "abstract": true, 87 "assert": true, 88 "boolean": true, 89 "break": true, 90 "byte": true, 91 "case": true, 92 "catch": true, 93 "char": true, 94 "class": true, 95 "const": true, 96 "continue": true, 97 "default": true, 98 "do": true, 99 "double": true, 100 "else": true, 101 "enum": true, 102 "extends": true, 103 "false": true, 104 "final": true, 105 "finally": true, 106 "float": true, 107 "for": true, 108 "goto": true, 109 "if": true, 110 "implements": true, 111 "import": true, 112 "instanceof": true, 113 "int": true, 114 "interface": true, 115 "long": true, 116 "native": true, 117 "new": true, 118 "null": true, 119 "package": true, 120 "private": true, 121 "protected": true, 122 "public": true, 123 "return": true, 124 "short": true, 125 "static": true, 126 "strictfp": true, 127 "super": true, 128 "switch": true, 129 "synchronized": true, 130 "this": true, 131 "throw": true, 132 "throws": true, 133 "transient": true, 134 "true": true, 135 "try": true, 136 "void": true, 137 "volatile": true, 138 "while": true} 139) 140 141type rtxtFile interface { 142 io.Reader 143 io.Closer 144} 145 146type resource struct { 147 ID string 148 resType string 149 varType string 150} 151 152func (r *resource) String() string { 153 return fmt.Sprintf("{%s %s %s}", r.varType, r.resType, r.ID) 154} 155 156// Init initializes finalrjar action. 157func Init() { 158 initOnce.Do(func() { 159 flag.StringVar(&pkg, "package", "", "Package for the R.jar") 160 flag.StringVar(&rtxts, "r_txts", "", "Comma separated list of R.txt files") 161 flag.StringVar(&outputRJar, "out_rjar", "", "Output R.jar path") 162 flag.StringVar(&rootPackage, "root_pkg", "mi.rjava", "Package to use for root R.java") 163 flag.StringVar(&jdk, "jdk", "", "Jdk path") 164 flag.StringVar(&jartool, "jartool", "", "Jartool path") 165 flag.StringVar(&targetLabel, "target_label", "", "The target label") 166 }) 167} 168 169func desc() string { 170 return "finalrjar creates a platform conform R.jar from R.txt files" 171} 172 173// Run is the entry point for finalrjar. Will exit on error. 174func Run() { 175 if err := doWork(pkg, rtxts, outputRJar, rootPackage, jdk, jartool, targetLabel); err != nil { 176 log.Fatalf("error creating final R.jar: %v", err) 177 } 178} 179 180func doWork(pkg, rtxts, outputRJar, rootPackage, jdk, jartool, targetLabel string) error { 181 pkgParts := strings.Split(pkg, ".") 182 // Check if the package is invalid. 183 if hasJavaReservedWord(pkgParts) { 184 return ziputils.EmptyZip(outputRJar) 185 } 186 187 rtxtFiles, err := openRtxts(strings.Split(rtxts, ",")) 188 if err != nil { 189 return err 190 } 191 192 resC := getIds(rtxtFiles) 193 // Resources need to be grouped by type to write the R.java classes. 194 resMap := groupResByType(resC) 195 196 srcDir, err := os.MkdirTemp("", "rjar") 197 if err != nil { 198 return err 199 } 200 defer os.RemoveAll(srcDir) 201 202 rJava, outRJava, err := createTmpRJava(srcDir, pkgParts) 203 if err != nil { 204 return err 205 } 206 defer outRJava.Close() 207 208 rootPkgParts := strings.Split(rootPackage, ".") 209 rootRJava, outRootRJava, err := createTmpRJava(srcDir, rootPkgParts) 210 if err != nil { 211 return err 212 } 213 defer outRootRJava.Close() 214 215 if err := writeRJavas(outRJava, outRootRJava, resMap, pkg, rootPackage); err != nil { 216 return err 217 } 218 219 fullRJar := filepath.Join(srcDir, "R.jar") 220 if err := compileRJar([]string{rJava, rootRJava}, fullRJar, jdk, jartool, targetLabel); err != nil { 221 return err 222 } 223 224 return filterZip(fullRJar, outputRJar, filepath.Join(rootPkgParts...)) 225} 226 227func getIds(rtxtFiles []rtxtFile) <-chan *resource { 228 // Sending all res to the same channel, even duplicates. 229 resC := make(chan *resource) 230 var wg sync.WaitGroup 231 wg.Add(len(rtxtFiles)) 232 233 for _, file := range rtxtFiles { 234 go func(file rtxtFile) { 235 defer wg.Done() 236 scanner := bufio.NewScanner(file) 237 for scanner.Scan() { 238 line := scanner.Text() 239 // Each line is in the following format: 240 // [int|int[]] resType resID value 241 // Ex: int anim abc_fade_in 0 242 parts := strings.Split(line, " ") 243 if len(parts) < 3 { 244 continue 245 } 246 // Aapt2 will sometime add resources containing the char '$'. 247 // Those should be ignored - they are derived from an actual resource. 248 if strings.Contains(parts[2], "$") { 249 continue 250 } 251 resC <- &resource{ID: parts[2], resType: parts[1], varType: parts[0]} 252 } 253 file.Close() 254 }(file) 255 } 256 257 go func() { 258 wg.Wait() 259 close(resC) 260 }() 261 262 return resC 263} 264 265func groupResByType(resC <-chan *resource) map[string][]*resource { 266 // Set of resType.ID seen to ignore duplicates from different R.txt files. 267 // Resources of different types can have the same ID, so we merge the values 268 // to get a unique string. Ex: integer.btn_background_alpa 269 seen := make(map[string]bool) 270 271 // Map of resource type to list of resources. 272 resMap := make(map[string][]*resource) 273 for res := range resC { 274 uniqueID := fmt.Sprintf("%s.%s", res.resType, res.ID) 275 if _, ok := seen[uniqueID]; ok { 276 continue 277 } 278 seen[uniqueID] = true 279 resMap[res.resType] = append(resMap[res.resType], res) 280 } 281 return resMap 282} 283 284func writeRJavas(outRJava, outRootRJava io.Writer, resMap map[string][]*resource, pkg, rootPackage string) error { 285 // The R.java points to the same resources ID in the root R.java. 286 // The root R.java uses 0 or null for simplicity and does not use final fields to avoid inlining. 287 // That way we can strip it from the compiled R.jar later and replace it with the real one. 288 rJavaWriter := bufio.NewWriter(outRJava) 289 rJavaWriter.WriteString(fmt.Sprintf("package %s;\n", pkg)) 290 rJavaWriter.WriteString("public class R {\n") 291 rootRJavaWriter := bufio.NewWriter(outRootRJava) 292 rootRJavaWriter.WriteString(fmt.Sprintf("package %s;\n", rootPackage)) 293 rootRJavaWriter.WriteString("public class R {\n") 294 295 for _, resType := range resTypes { 296 if resources, ok := resMap[resType]; ok { 297 rJavaWriter.WriteString(fmt.Sprintf(" public static class %s {\n", resType)) 298 rootRJavaWriter.WriteString(fmt.Sprintf(" public static class %s {\n", resType)) 299 rootID := fmt.Sprintf("%s.R.%s.", rootPackage, resType) 300 301 // Sorting resources before writing to class 302 sort.Slice(resources, func(i, j int) bool { 303 return resources[i].ID < resources[j].ID 304 }) 305 for _, res := range resources { 306 defaultValue := "0" 307 if res.varType == "int[]" { 308 defaultValue = "null" 309 } 310 rJavaWriter.WriteString(fmt.Sprintf(" public static final %s %s=%s%s;\n", res.varType, res.ID, rootID, res.ID)) 311 rootRJavaWriter.WriteString(fmt.Sprintf(" public static %s %s=%s;\n", res.varType, res.ID, defaultValue)) 312 } 313 rJavaWriter.WriteString(" }\n") 314 rootRJavaWriter.WriteString(" }\n") 315 } 316 } 317 rJavaWriter.WriteString("}\n") 318 rootRJavaWriter.WriteString("}\n") 319 320 if err := rJavaWriter.Flush(); err != nil { 321 return err 322 } 323 return rootRJavaWriter.Flush() 324} 325 326func createTmpRJava(srcDir string, pkgParts []string) (string, *os.File, error) { 327 pkgDir := filepath.Join(append([]string{srcDir}, pkgParts...)...) 328 if err := os.MkdirAll(pkgDir, 0777); err != nil { 329 return "", nil, err 330 } 331 file := filepath.Join(pkgDir, "R.java") 332 out, err := os.Create(file) 333 return file, out, err 334} 335 336func openRtxts(filePaths []string) ([]rtxtFile, error) { 337 var rtxtFiles []rtxtFile 338 for _, filePath := range filePaths { 339 in, err := os.Open(filePath) 340 if err != nil { 341 return nil, err 342 } 343 rtxtFiles = append(rtxtFiles, in) 344 } 345 return rtxtFiles, nil 346 347} 348 349func createOuput(output string) (io.Writer, error) { 350 if _, err := os.Lstat(output); err == nil { 351 if err := os.Remove(output); err != nil { 352 return nil, err 353 } 354 } 355 if err := os.MkdirAll(filepath.Dir(output), 0777); err != nil { 356 return nil, err 357 } 358 359 return os.Create(output) 360} 361 362func filterZip(in, output, ignorePrefix string) error { 363 w, err := createOuput(output) 364 if err != nil { 365 return err 366 } 367 368 zipOut := zip.NewWriter(w) 369 defer zipOut.Close() 370 371 zipIn, err := zip.OpenReader(in) 372 if err != nil { 373 return err 374 } 375 defer zipIn.Close() 376 377 for _, f := range zipIn.File { 378 // Ignoring the dummy root R.java. 379 if strings.HasPrefix(f.Name, ignorePrefix) { 380 continue 381 } 382 reader, err := f.Open() 383 if err != nil { 384 return err 385 } 386 if err := writeToZip(zipOut, reader, f.Name, f.Method); err != nil { 387 return err 388 } 389 if err := reader.Close(); err != nil { 390 return err 391 } 392 } 393 return nil 394} 395 396func writeToZip(out *zip.Writer, in io.Reader, name string, method uint16) error { 397 writer, err := out.CreateHeader(&zip.FileHeader{ 398 Name: name, 399 Method: method, 400 }) 401 if err != nil { 402 return err 403 } 404 405 if !strings.HasSuffix(name, "/") { 406 if _, err := io.Copy(writer, in); err != nil { 407 return err 408 } 409 } 410 return nil 411} 412 413func compileRJar(srcs []string, rjar, jdk, jartool string, targetLabel string) error { 414 control, err := os.CreateTemp("", "control") 415 if err != nil { 416 return err 417 } 418 defer os.Remove(control.Name()) 419 420 args := []string{"--javacopts", 421 "-source", "8", 422 "-target", "8", 423 "-nowarn", "--", "--sources"} 424 args = append(args, srcs...) 425 args = append(args, 426 "--strict_java_deps", "ERROR", 427 "--output", rjar) 428 if len(targetLabel) > 0 { 429 args = append(args, "--target_label", targetLabel) 430 } 431 if _, err := fmt.Fprint(control, strings.Join(args, "\n")); err != nil { 432 return err 433 } 434 if err := control.Sync(); err != nil { 435 return err 436 } 437 c, err := exec.Command(jdk, "-jar", jartool, fmt.Sprintf("@%s", control.Name())).CombinedOutput() 438 if err != nil { 439 return fmt.Errorf("error compiling R.jar (using command: %s): %v", c, err) 440 } 441 return nil 442} 443 444func hasJavaReservedWord(parts []string) bool { 445 for _, p := range parts { 446 if javaReserved[p] { 447 return true 448 } 449 } 450 return false 451} 452