| // 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 finalrjar generates a valid final R.jar. |
| package finalrjar |
| |
| import ( |
| "archive/zip" |
| "bufio" |
| "flag" |
| "fmt" |
| "io" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "sort" |
| "strings" |
| "sync" |
| |
| "src/common/golang/ziputils" |
| "src/tools/ak/types" |
| ) |
| |
| var ( |
| // Cmd defines the command. |
| Cmd = types.Command{ |
| Init: Init, |
| Run: Run, |
| Desc: desc, |
| Flags: []string{"package", "r_txts", "out_r_java", "root_pkg", "jdk", "jartool", "target_label"}, |
| } |
| |
| // Variables to hold flag values. |
| pkg string |
| rtxts string |
| outputRJar string |
| rootPackage string |
| jdk string |
| jartool string |
| targetLabel string |
| |
| initOnce sync.Once |
| |
| resTypes = []string{ |
| "anim", |
| "animator", |
| "array", |
| "attr", |
| "^attr-private", |
| "bool", |
| "color", |
| "configVarying", |
| "dimen", |
| "drawable", |
| "fraction", |
| "font", |
| "id", |
| "integer", |
| "interpolator", |
| "layout", |
| "menu", |
| "mipmap", |
| "navigation", |
| "plurals", |
| "raw", |
| "string", |
| "style", |
| "styleable", |
| "transition", |
| "xml", |
| } |
| |
| javaReserved = map[string]bool{ |
| "abstract": true, |
| "assert": true, |
| "boolean": true, |
| "break": true, |
| "byte": true, |
| "case": true, |
| "catch": true, |
| "char": true, |
| "class": true, |
| "const": true, |
| "continue": true, |
| "default": true, |
| "do": true, |
| "double": true, |
| "else": true, |
| "enum": true, |
| "extends": true, |
| "false": true, |
| "final": true, |
| "finally": true, |
| "float": true, |
| "for": true, |
| "goto": true, |
| "if": true, |
| "implements": true, |
| "import": true, |
| "instanceof": true, |
| "int": true, |
| "interface": true, |
| "long": true, |
| "native": true, |
| "new": true, |
| "null": true, |
| "package": true, |
| "private": true, |
| "protected": true, |
| "public": true, |
| "return": true, |
| "short": true, |
| "static": true, |
| "strictfp": true, |
| "super": true, |
| "switch": true, |
| "synchronized": true, |
| "this": true, |
| "throw": true, |
| "throws": true, |
| "transient": true, |
| "true": true, |
| "try": true, |
| "void": true, |
| "volatile": true, |
| "while": true} |
| ) |
| |
| type rtxtFile interface { |
| io.Reader |
| io.Closer |
| } |
| |
| type resource struct { |
| ID string |
| resType string |
| varType string |
| } |
| |
| func (r *resource) String() string { |
| return fmt.Sprintf("{%s %s %s}", r.varType, r.resType, r.ID) |
| } |
| |
| // Init initializes finalrjar action. |
| func Init() { |
| initOnce.Do(func() { |
| flag.StringVar(&pkg, "package", "", "Package for the R.jar") |
| flag.StringVar(&rtxts, "r_txts", "", "Comma separated list of R.txt files") |
| flag.StringVar(&outputRJar, "out_rjar", "", "Output R.jar path") |
| flag.StringVar(&rootPackage, "root_pkg", "mi.rjava", "Package to use for root R.java") |
| flag.StringVar(&jdk, "jdk", "", "Jdk path") |
| flag.StringVar(&jartool, "jartool", "", "Jartool path") |
| flag.StringVar(&targetLabel, "target_label", "", "The target label") |
| }) |
| } |
| |
| func desc() string { |
| return "finalrjar creates a platform conform R.jar from R.txt files" |
| } |
| |
| // Run is the entry point for finalrjar. Will exit on error. |
| func Run() { |
| if err := doWork(pkg, rtxts, outputRJar, rootPackage, jdk, jartool, targetLabel); err != nil { |
| log.Fatalf("error creating final R.jar: %v", err) |
| } |
| } |
| |
| func doWork(pkg, rtxts, outputRJar, rootPackage, jdk, jartool, targetLabel string) error { |
| pkgParts := strings.Split(pkg, ".") |
| // Check if the package is invalid. |
| if hasJavaReservedWord(pkgParts) { |
| return ziputils.EmptyZip(outputRJar) |
| } |
| |
| rtxtFiles, err := openRtxts(strings.Split(rtxts, ",")) |
| if err != nil { |
| return err |
| } |
| |
| resC := getIds(rtxtFiles) |
| // Resources need to be grouped by type to write the R.java classes. |
| resMap := groupResByType(resC) |
| |
| srcDir, err := os.MkdirTemp("", "rjar") |
| if err != nil { |
| return err |
| } |
| defer os.RemoveAll(srcDir) |
| |
| rJava, outRJava, err := createTmpRJava(srcDir, pkgParts) |
| if err != nil { |
| return err |
| } |
| defer outRJava.Close() |
| |
| rootPkgParts := strings.Split(rootPackage, ".") |
| rootRJava, outRootRJava, err := createTmpRJava(srcDir, rootPkgParts) |
| if err != nil { |
| return err |
| } |
| defer outRootRJava.Close() |
| |
| if err := writeRJavas(outRJava, outRootRJava, resMap, pkg, rootPackage); err != nil { |
| return err |
| } |
| |
| fullRJar := filepath.Join(srcDir, "R.jar") |
| if err := compileRJar([]string{rJava, rootRJava}, fullRJar, jdk, jartool, targetLabel); err != nil { |
| return err |
| } |
| |
| return filterZip(fullRJar, outputRJar, filepath.Join(rootPkgParts...)) |
| } |
| |
| func getIds(rtxtFiles []rtxtFile) <-chan *resource { |
| // Sending all res to the same channel, even duplicates. |
| resC := make(chan *resource) |
| var wg sync.WaitGroup |
| wg.Add(len(rtxtFiles)) |
| |
| for _, file := range rtxtFiles { |
| go func(file rtxtFile) { |
| defer wg.Done() |
| scanner := bufio.NewScanner(file) |
| for scanner.Scan() { |
| line := scanner.Text() |
| // Each line is in the following format: |
| // [int|int[]] resType resID value |
| // Ex: int anim abc_fade_in 0 |
| parts := strings.Split(line, " ") |
| if len(parts) < 3 { |
| continue |
| } |
| // Aapt2 will sometime add resources containing the char '$'. |
| // Those should be ignored - they are derived from an actual resource. |
| if strings.Contains(parts[2], "$") { |
| continue |
| } |
| resC <- &resource{ID: parts[2], resType: parts[1], varType: parts[0]} |
| } |
| file.Close() |
| }(file) |
| } |
| |
| go func() { |
| wg.Wait() |
| close(resC) |
| }() |
| |
| return resC |
| } |
| |
| func groupResByType(resC <-chan *resource) map[string][]*resource { |
| // Set of resType.ID seen to ignore duplicates from different R.txt files. |
| // Resources of different types can have the same ID, so we merge the values |
| // to get a unique string. Ex: integer.btn_background_alpa |
| seen := make(map[string]bool) |
| |
| // Map of resource type to list of resources. |
| resMap := make(map[string][]*resource) |
| for res := range resC { |
| uniqueID := fmt.Sprintf("%s.%s", res.resType, res.ID) |
| if _, ok := seen[uniqueID]; ok { |
| continue |
| } |
| seen[uniqueID] = true |
| resMap[res.resType] = append(resMap[res.resType], res) |
| } |
| return resMap |
| } |
| |
| func writeRJavas(outRJava, outRootRJava io.Writer, resMap map[string][]*resource, pkg, rootPackage string) error { |
| // The R.java points to the same resources ID in the root R.java. |
| // The root R.java uses 0 or null for simplicity and does not use final fields to avoid inlining. |
| // That way we can strip it from the compiled R.jar later and replace it with the real one. |
| rJavaWriter := bufio.NewWriter(outRJava) |
| rJavaWriter.WriteString(fmt.Sprintf("package %s;\n", pkg)) |
| rJavaWriter.WriteString("public class R {\n") |
| rootRJavaWriter := bufio.NewWriter(outRootRJava) |
| rootRJavaWriter.WriteString(fmt.Sprintf("package %s;\n", rootPackage)) |
| rootRJavaWriter.WriteString("public class R {\n") |
| |
| for _, resType := range resTypes { |
| if resources, ok := resMap[resType]; ok { |
| rJavaWriter.WriteString(fmt.Sprintf(" public static class %s {\n", resType)) |
| rootRJavaWriter.WriteString(fmt.Sprintf(" public static class %s {\n", resType)) |
| rootID := fmt.Sprintf("%s.R.%s.", rootPackage, resType) |
| |
| // Sorting resources before writing to class |
| sort.Slice(resources, func(i, j int) bool { |
| return resources[i].ID < resources[j].ID |
| }) |
| for _, res := range resources { |
| defaultValue := "0" |
| if res.varType == "int[]" { |
| defaultValue = "null" |
| } |
| rJavaWriter.WriteString(fmt.Sprintf(" public static final %s %s=%s%s;\n", res.varType, res.ID, rootID, res.ID)) |
| rootRJavaWriter.WriteString(fmt.Sprintf(" public static %s %s=%s;\n", res.varType, res.ID, defaultValue)) |
| } |
| rJavaWriter.WriteString(" }\n") |
| rootRJavaWriter.WriteString(" }\n") |
| } |
| } |
| rJavaWriter.WriteString("}\n") |
| rootRJavaWriter.WriteString("}\n") |
| |
| if err := rJavaWriter.Flush(); err != nil { |
| return err |
| } |
| return rootRJavaWriter.Flush() |
| } |
| |
| func createTmpRJava(srcDir string, pkgParts []string) (string, *os.File, error) { |
| pkgDir := filepath.Join(append([]string{srcDir}, pkgParts...)...) |
| if err := os.MkdirAll(pkgDir, 0777); err != nil { |
| return "", nil, err |
| } |
| file := filepath.Join(pkgDir, "R.java") |
| out, err := os.Create(file) |
| return file, out, err |
| } |
| |
| func openRtxts(filePaths []string) ([]rtxtFile, error) { |
| var rtxtFiles []rtxtFile |
| for _, filePath := range filePaths { |
| in, err := os.Open(filePath) |
| if err != nil { |
| return nil, err |
| } |
| rtxtFiles = append(rtxtFiles, in) |
| } |
| return rtxtFiles, nil |
| |
| } |
| |
| func createOuput(output string) (io.Writer, error) { |
| if _, err := os.Lstat(output); err == nil { |
| if err := os.Remove(output); err != nil { |
| return nil, err |
| } |
| } |
| if err := os.MkdirAll(filepath.Dir(output), 0777); err != nil { |
| return nil, err |
| } |
| |
| return os.Create(output) |
| } |
| |
| func filterZip(in, output, ignorePrefix string) error { |
| w, err := createOuput(output) |
| if err != nil { |
| return err |
| } |
| |
| zipOut := zip.NewWriter(w) |
| defer zipOut.Close() |
| |
| zipIn, err := zip.OpenReader(in) |
| if err != nil { |
| return err |
| } |
| defer zipIn.Close() |
| |
| for _, f := range zipIn.File { |
| // Ignoring the dummy root R.java. |
| if strings.HasPrefix(f.Name, ignorePrefix) { |
| continue |
| } |
| reader, err := f.Open() |
| if err != nil { |
| return err |
| } |
| if err := writeToZip(zipOut, reader, f.Name, f.Method); err != nil { |
| return err |
| } |
| if err := reader.Close(); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func writeToZip(out *zip.Writer, in io.Reader, name string, method uint16) error { |
| writer, err := out.CreateHeader(&zip.FileHeader{ |
| Name: name, |
| Method: method, |
| }) |
| if err != nil { |
| return err |
| } |
| |
| if !strings.HasSuffix(name, "/") { |
| if _, err := io.Copy(writer, in); err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func compileRJar(srcs []string, rjar, jdk, jartool string, targetLabel string) error { |
| control, err := os.CreateTemp("", "control") |
| if err != nil { |
| return err |
| } |
| defer os.Remove(control.Name()) |
| |
| args := []string{"--javacopts", |
| "-source", "8", |
| "-target", "8", |
| "-nowarn", "--", "--sources"} |
| args = append(args, srcs...) |
| args = append(args, |
| "--strict_java_deps", "ERROR", |
| "--output", rjar) |
| if len(targetLabel) > 0 { |
| args = append(args, "--target_label", targetLabel) |
| } |
| if _, err := fmt.Fprint(control, strings.Join(args, "\n")); err != nil { |
| return err |
| } |
| if err := control.Sync(); err != nil { |
| return err |
| } |
| c, err := exec.Command(jdk, "-jar", jartool, fmt.Sprintf("@%s", control.Name())).CombinedOutput() |
| if err != nil { |
| return fmt.Errorf("error compiling R.jar (using command: %s): %v", c, err) |
| } |
| return nil |
| } |
| |
| func hasJavaReservedWord(parts []string) bool { |
| for _, p := range parts { |
| if javaReserved[p] { |
| return true |
| } |
| } |
| return false |
| } |