// Copyright 2021 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 extractaar extracts files from an aar. package extractaar import ( "archive/zip" "errors" "flag" "fmt" "io" "log" "os" "path/filepath" "strings" "sync" "src/tools/ak/types" ) // A tristate may be true, false, or unset type tristate int func (t tristate) isSet() bool { return t == tsTrue || t == tsFalse } func (t tristate) value() bool { return t == tsTrue } const ( tsTrue = 1 tsFalse = -1 manifest = iota res assets ) var ( // Cmd defines the command to run the extractor. Cmd = types.Command{ Init: Init, Run: Run, Desc: desc, Flags: []string{ "aar", "label", "out_manifest", "out_res_dir", "out_assets_dir", "has_res", "has_assets", }, } aar string label string outputManifest string outputResDir string outputAssetsDir string hasRes int hasAssets int initOnce sync.Once ) // Init initializes the extractor. func Init() { initOnce.Do(func() { flag.StringVar(&aar, "aar", "", "Path to the aar") flag.StringVar(&label, "label", "", "Target's label") flag.StringVar(&outputManifest, "out_manifest", "", "Output manifest") flag.StringVar(&outputResDir, "out_res_dir", "", "Output resources directory") flag.StringVar(&outputAssetsDir, "out_assets_dir", "", "Output assets directory") flag.IntVar(&hasRes, "has_res", 0, "Whether the aar has resources") flag.IntVar(&hasAssets, "has_assets", 0, "Whether the aar has assets") }) } func desc() string { return "Extracts files from an AAR" } type aarFile struct { path string relPath string } func (file *aarFile) String() string { return fmt.Sprintf("%s:%s", file.path, file.relPath) } type toCopy struct { src string dest string } // Run runs the extractor func Run() { if err := doWork(aar, label, outputManifest, outputResDir, outputAssetsDir, hasRes, hasAssets); err != nil { log.Fatal(err) } } func doWork(aar, label, outputManifest, outputResDir, outputAssetsDir string, hasRes, hasAssets int) error { tmpDir, err := os.MkdirTemp("", "extractaar_") if err != nil { return err } defer os.RemoveAll(tmpDir) files, err := extractAAR(aar, tmpDir) if err != nil { return err } validators := map[int]validator{ manifest: manifestValidator{dest: outputManifest}, res: resourceValidator{dest: outputResDir, hasRes: tristate(hasRes), ruleAttr: "has_res"}, assets: resourceValidator{dest: outputAssetsDir, hasRes: tristate(hasAssets), ruleAttr: "has_assets"}, } var filesToCopy []*toCopy var validationErrs []*BuildozerError for fileType, files := range groupAARFiles(files) { validatedFiles, err := validators[fileType].validate(files) if err != nil { validationErrs = append(validationErrs, err) continue } filesToCopy = append(filesToCopy, validatedFiles...) } if len(validationErrs) != 0 { return errors.New(mergeBuildozerErrors(label, validationErrs)) } for _, file := range filesToCopy { if err := copyFile(file.src, file.dest); err != nil { return err } } // TODO(ostonge): Add has_res/has_assets attr to avoid having to do this // We need to create at least one file so that Bazel does not complain // that the output tree artifact was not created. if err := createIfEmpty(outputResDir, "res/values/empty.xml", ""); err != nil { return err } // aapt will ignore this file and not print an error message, because it // thinks that it is a swap file if err := createIfEmpty(outputAssetsDir, "assets/empty_asset_generated_by_bazel~", ""); err != nil { return err } return nil } func groupAARFiles(aarFiles []*aarFile) map[int][]*aarFile { // Map of file type to channel of aarFile filesMap := make(map[int][]*aarFile) for _, fileType := range []int{manifest, res, assets} { filesMap[fileType] = make([]*aarFile, 0) } for _, file := range aarFiles { if file.relPath == "AndroidManifest.xml" { filesMap[manifest] = append(filesMap[manifest], file) } else if strings.HasPrefix(file.relPath, "res"+string(os.PathSeparator)) { filesMap[res] = append(filesMap[res], file) } else if strings.HasPrefix(file.relPath, "assets"+string(os.PathSeparator)) { filesMap[assets] = append(filesMap[assets], file) } // TODO(ostonge): support jar and aidl files } return filesMap } func extractAAR(aar string, dest string) ([]*aarFile, error) { reader, err := zip.OpenReader(aar) if err != nil { return nil, err } defer reader.Close() var files []*aarFile for _, f := range reader.File { if f.FileInfo().IsDir() { continue } extractedPath := filepath.Join(dest, f.Name) if err := extractFile(f, extractedPath); err != nil { return nil, err } files = append(files, &aarFile{path: extractedPath, relPath: f.Name}) } return files, nil } func extractFile(file *zip.File, dest string) error { if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { return err } outFile, err := os.OpenFile(dest, os.O_WRONLY|os.O_CREATE, file.Mode()) if err != nil { return err } defer outFile.Close() rc, err := file.Open() if err != nil { return err } defer rc.Close() _, err = io.Copy(outFile, rc) if err != nil { return err } return nil } func copyFile(name, dest string) error { in, err := os.Open(name) if err != nil { return err } defer in.Close() if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { return err } out, err := os.Create(dest) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) if err != nil { return err } return nil } func dirIsEmpty(dir string) (bool, error) { f, err := os.Open(dir) if os.IsNotExist(err) { return true, nil } if err != nil { return false, err } defer f.Close() _, err = f.Readdirnames(1) if err == io.EOF { return true, nil } return false, err } // Create the file with the content if the directory is empty or does not exists func createIfEmpty(dir, filename, content string) error { isEmpty, err := dirIsEmpty(dir) if err != nil { return err } if isEmpty { dest := filepath.Join(dir, filename) if err := os.MkdirAll(filepath.Dir(dest), os.ModePerm); err != nil { return err } return os.WriteFile(dest, []byte(content), 0644) } return nil }