blob: 2a431faab4ac1dd65c5831dd62e2f483c8e0f3b6 [file]
// 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", "<resources/>"); 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
}