xref: /aosp_15_r20/external/bazelbuild-rules_android/src/tools/ak/extractaar/extractaar.go (revision 9e965d6fece27a77de5377433c2f7e6999b8cc0b)
1// Copyright 2021 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 extractaar extracts files from an aar.
16package extractaar
17
18import (
19	"archive/zip"
20	"errors"
21	"flag"
22	"fmt"
23	"io"
24	"log"
25	"os"
26	"path/filepath"
27	"strings"
28	"sync"
29
30	"src/tools/ak/types"
31)
32
33// A tristate may be true, false, or unset
34type tristate int
35
36func (t tristate) isSet() bool {
37	return t == tsTrue || t == tsFalse
38}
39
40func (t tristate) value() bool {
41	return t == tsTrue
42}
43
44const (
45	tsTrue  = 1
46	tsFalse = -1
47
48	manifest = iota
49	res
50	assets
51)
52
53var (
54	// Cmd defines the command to run the extractor.
55	Cmd = types.Command{
56		Init: Init,
57		Run:  Run,
58		Desc: desc,
59		Flags: []string{
60			"aar", "label",
61			"out_manifest", "out_res_dir", "out_assets_dir",
62			"has_res", "has_assets",
63		},
64	}
65
66	aar             string
67	label           string
68	outputManifest  string
69	outputResDir    string
70	outputAssetsDir string
71	hasRes          int
72	hasAssets       int
73
74	initOnce sync.Once
75)
76
77// Init initializes the extractor.
78func Init() {
79	initOnce.Do(func() {
80		flag.StringVar(&aar, "aar", "", "Path to the aar")
81		flag.StringVar(&label, "label", "", "Target's label")
82		flag.StringVar(&outputManifest, "out_manifest", "", "Output manifest")
83		flag.StringVar(&outputResDir, "out_res_dir", "", "Output resources directory")
84		flag.StringVar(&outputAssetsDir, "out_assets_dir", "", "Output assets directory")
85		flag.IntVar(&hasRes, "has_res", 0, "Whether the aar has resources")
86		flag.IntVar(&hasAssets, "has_assets", 0, "Whether the aar has assets")
87	})
88}
89
90func desc() string {
91	return "Extracts files from an AAR"
92}
93
94type aarFile struct {
95	path    string
96	relPath string
97}
98
99func (file *aarFile) String() string {
100	return fmt.Sprintf("%s:%s", file.path, file.relPath)
101}
102
103type toCopy struct {
104	src  string
105	dest string
106}
107
108// Run runs the extractor
109func Run() {
110	if err := doWork(aar, label, outputManifest, outputResDir, outputAssetsDir, hasRes, hasAssets); err != nil {
111		log.Fatal(err)
112	}
113}
114
115func doWork(aar, label, outputManifest, outputResDir, outputAssetsDir string, hasRes, hasAssets int) error {
116	tmpDir, err := os.MkdirTemp("", "extractaar_")
117	if err != nil {
118		return err
119	}
120	defer os.RemoveAll(tmpDir)
121
122	files, err := extractAAR(aar, tmpDir)
123	if err != nil {
124		return err
125	}
126
127	validators := map[int]validator{
128		manifest: manifestValidator{dest: outputManifest},
129		res:      resourceValidator{dest: outputResDir, hasRes: tristate(hasRes), ruleAttr: "has_res"},
130		assets:   resourceValidator{dest: outputAssetsDir, hasRes: tristate(hasAssets), ruleAttr: "has_assets"},
131	}
132
133	var filesToCopy []*toCopy
134	var validationErrs []*BuildozerError
135	for fileType, files := range groupAARFiles(files) {
136		validatedFiles, err := validators[fileType].validate(files)
137		if err != nil {
138			validationErrs = append(validationErrs, err)
139			continue
140		}
141		filesToCopy = append(filesToCopy, validatedFiles...)
142	}
143
144	if len(validationErrs) != 0 {
145		return errors.New(mergeBuildozerErrors(label, validationErrs))
146	}
147
148	for _, file := range filesToCopy {
149		if err := copyFile(file.src, file.dest); err != nil {
150			return err
151		}
152	}
153
154	// TODO(ostonge): Add has_res/has_assets attr to avoid having to do this
155	// We need to create at least one file so that Bazel does not complain
156	// that the output tree artifact was not created.
157	if err := createIfEmpty(outputResDir, "res/values/empty.xml", "<resources/>"); err != nil {
158		return err
159	}
160	// aapt will ignore this file and not print an error message, because it
161	// thinks that it is a swap file
162	if err := createIfEmpty(outputAssetsDir, "assets/empty_asset_generated_by_bazel~", ""); err != nil {
163		return err
164	}
165	return nil
166}
167
168func groupAARFiles(aarFiles []*aarFile) map[int][]*aarFile {
169	// Map of file type to channel of aarFile
170	filesMap := make(map[int][]*aarFile)
171	for _, fileType := range []int{manifest, res, assets} {
172		filesMap[fileType] = make([]*aarFile, 0)
173	}
174
175	for _, file := range aarFiles {
176		if file.relPath == "AndroidManifest.xml" {
177			filesMap[manifest] = append(filesMap[manifest], file)
178		} else if strings.HasPrefix(file.relPath, "res"+string(os.PathSeparator)) {
179			filesMap[res] = append(filesMap[res], file)
180		} else if strings.HasPrefix(file.relPath, "assets"+string(os.PathSeparator)) {
181			filesMap[assets] = append(filesMap[assets], file)
182		}
183		// TODO(ostonge): support jar and aidl files
184	}
185	return filesMap
186}
187
188func extractAAR(aar string, dest string) ([]*aarFile, error) {
189	reader, err := zip.OpenReader(aar)
190	if err != nil {
191		return nil, err
192	}
193	defer reader.Close()
194
195	var files []*aarFile
196	for _, f := range reader.File {
197		if f.FileInfo().IsDir() {
198			continue
199		}
200		extractedPath := filepath.Join(dest, f.Name)
201		if err := extractFile(f, extractedPath); err != nil {
202			return nil, err
203		}
204		files = append(files, &aarFile{path: extractedPath, relPath: f.Name})
205	}
206	return files, nil
207}
208
209func extractFile(file *zip.File, dest string) error {
210	if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
211		return err
212	}
213	outFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, file.Mode())
214	if err != nil {
215		return err
216	}
217	defer outFile.Close()
218
219	rc, err := file.Open()
220	if err != nil {
221		return err
222	}
223	defer rc.Close()
224
225	_, err = io.Copy(outFile, rc)
226	if err != nil {
227		return err
228	}
229	return nil
230}
231
232func copyFile(name, dest string) error {
233	in, err := os.Open(name)
234	if err != nil {
235		return err
236	}
237	defer in.Close()
238
239	if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
240		return err
241	}
242	out, err := os.Create(dest)
243	if err != nil {
244		return err
245	}
246	defer out.Close()
247
248	_, err = io.Copy(out, in)
249	if err != nil {
250		return err
251	}
252	return nil
253}
254
255func dirIsEmpty(dir string) (bool, error) {
256	f, err := os.Open(dir)
257	if os.IsNotExist(err) {
258		return true, nil
259	}
260	if err != nil {
261		return false, err
262	}
263	defer f.Close()
264
265	_, err = f.Readdirnames(1)
266	if err == io.EOF {
267		return true, nil
268	}
269	return false, err
270}
271
272// Create the file with the content if the directory is empty or does not exists
273func createIfEmpty(dir, filename, content string) error {
274	isEmpty, err := dirIsEmpty(dir)
275	if err != nil {
276		return err
277	}
278	if isEmpty {
279		dest := filepath.Join(dir, filename)
280		if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil {
281			return err
282		}
283		return os.WriteFile(dest, []byte(content), 0644)
284	}
285	return nil
286}
287