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