xref: /aosp_15_r20/external/bazelbuild-rules_android/src/tools/ak/finalrjar/finalrjar.go (revision 9e965d6fece27a77de5377433c2f7e6999b8cc0b)
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