Skip to content
This repository was archived by the owner on Mar 26, 2024. It is now read-only.

Commit 6858e08

Browse files
committed
Added APK size analyzer
1 parent 728b23b commit 6858e08

15 files changed

+206
-104
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ dist
33
*.vsix
44
.taskkey
55
**/.taskkey
6+
.DS_Store
7+
**/.DS_Store
8+
test/assets/extracted/
9+

downloadAndroidSdk.ts

Lines changed: 0 additions & 51 deletions
This file was deleted.

package-lock.json

Lines changed: 24 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,13 @@
44
"description": "Azure DevOps task to measure the size in Android app size by looking at 2 given APKs and AABs",
55
"main": "index.js",
66
"scripts": {
7-
"postinstall": "npm run download-sdk-all",
87
"build": "npm run build-ts && npm run copy-files",
98
"adoTask": "npm run build && node dist/src/ado-index.js",
109
"build-ts": "tsc",
1110
"copy-files": "ts-node copyFiles.ts",
1211
"test": "npm run build && mocha dist/test/**/*.js",
1312
"bundle": "npm run build && npm run bundle-ado-task",
1413
"bundle-ado-task": "tfx extension create --manifest-globs ado-extension.json",
15-
"download-sdk-windows": "ts-node downloadAndroidSdk.ts Windows_NT",
16-
"download-sdk-linux": "ts-node downloadAndroidSdk.ts Linux",
17-
"download-sdk-mac": "ts-node downloadAndroidSdk.ts Darwin",
18-
"download-sdk-all": "npm run download-sdk-windows && npm run download-sdk-linux && npm run download-sdk-mac",
1914
"bump-major": "ts-node bumpVersion.ts major",
2015
"bump-minor": "ts-node bumpVersion.ts minor",
2116
"bump-patch": "ts-node bumpVersion.ts patch"
@@ -37,7 +32,11 @@
3732
},
3833
"homepage": "https://github.com/microsoft/android-app-size-ci#readme",
3934
"dependencies": {
40-
"azure-pipelines-task-lib": "^2.9.3"
35+
"@types/adm-zip": "^0.4.32",
36+
"@types/line-reader": "0.0.28",
37+
"adm-zip": "^0.4.13",
38+
"azure-pipelines-task-lib": "^2.9.3",
39+
"line-reader": "^0.4.0"
4140
},
4241
"devDependencies": {
4342
"@types/download": "^6.2.4",

src/adoTask/adoTaskRunner.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,28 @@
1-
import * as tl from 'azure-pipelines-task-lib/task';
2-
import * as shell from "shelljs";
3-
import * as os from 'os';
4-
import * as path from 'path';
1+
import * as adoTask from 'azure-pipelines-task-lib/task';
2+
import * as util from 'util';
3+
import ApkAnalyzer from '../apkAnalyzer/ApkAnalyzer';
54

65
export default class AdoTaskRunner {
76

87
public async run() {
9-
const platform = os.type();
10-
11-
const apkanalyzerPath = path.join('dist', 'src', 'bin', platform, 'tools', 'bin', 'apkanalyzer');
12-
const testApkPath = path.join('test', 'assets', 'test.apk');
13-
14-
shell.exec(apkanalyzerPath + ' apk download-size ' + testApkPath);
15-
168
try {
17-
const inputString: string | undefined = tl.getInput('baseAppPath', true);
18-
if (inputString == 'bad') {
19-
tl.setResult(tl.TaskResult.Failed, 'Bad input was given');
9+
const baseAppPath: string | undefined = adoTask.getInput('baseAppPath', true);
10+
const targetAppPath: string | undefined = adoTask.getInput('targetAppPath', true);
11+
const summaryOutputPath: string | undefined = adoTask.getInput('summaryOutputPath', false);
12+
13+
if (util.isUndefined(baseAppPath) || util.isUndefined(targetAppPath)) {
14+
adoTask.setResult(adoTask.TaskResult.Failed, 'Invalid app paths supplied!');
2015
return;
2116
}
22-
console.log('Hello', inputString);
17+
18+
const baseSizeSummary = await new ApkAnalyzer().analyse(baseAppPath);
19+
const targetSizeSummary = await new ApkAnalyzer().analyse(targetAppPath);
20+
21+
console.log(baseSizeSummary);
22+
console.log(targetSizeSummary);
2323
}
2424
catch (err) {
25-
tl.setResult(tl.TaskResult.Failed, err.message);
25+
adoTask.setResult(adoTask.TaskResult.Failed, err.message);
2626
}
2727
}
2828

src/apkAnalyzer/ApkAnalyzer.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import AdmZip from 'adm-zip';
2+
import * as path from 'path'
3+
import * as util from 'util';
4+
import MetaMfParser from './MetaMfParser';
5+
import ApkSizeSummary from './ApkSizeSummary';
6+
import { FilesSizeCalculator } from './FilesSizeCalculator';
7+
8+
export default class ApkAnalyzer {
9+
10+
public async analyse(apkPath: string, workingDir?: string) : Promise<ApkSizeSummary> {
11+
if (util.isUndefined(workingDir)) {
12+
workingDir = path.join(path.dirname(apkPath), 'extracted');
13+
}
14+
15+
// Extract the apk file
16+
const apkFile = new AdmZip(apkPath);
17+
console.log('Extracting APK file to ' + workingDir);
18+
apkFile.extractAllTo(workingDir, true);
19+
20+
// Parse the IMF file
21+
const mfParser = new MetaMfParser(workingDir);
22+
await mfParser.parse();
23+
24+
// Build Apk size summary
25+
const fileSizeCalc = new FilesSizeCalculator();
26+
const sizeSummary = new ApkSizeSummary();
27+
28+
sizeSummary.apkFile = fileSizeCalc.getFileSize(apkPath);
29+
sizeSummary.arscFile = fileSizeCalc.getFilesSize(mfParser.getFiles('.arsc'));
30+
sizeSummary.dexFiles = fileSizeCalc.getFilesSize(mfParser.getFiles('.dex'));
31+
sizeSummary.nativeLibs = fileSizeCalc.getFilesSize(mfParser.getFiles('.so'));
32+
sizeSummary.installSize = sizeSummary.apkFile + sizeSummary.dexFiles;
33+
34+
return sizeSummary;
35+
}
36+
37+
}

src/apkAnalyzer/ApkSizeSummary.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default class ApkSizeSummary {
2+
apkFile: number = 0;
3+
installSize: number = 0;
4+
dexFiles: number = 0;
5+
arscFile: number = 0;
6+
nativeLibs: number = 0;
7+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Calculates the cummulative size of given file(s)
3+
*/
4+
import * as fs from 'fs';
5+
import * as util from 'util';
6+
7+
export class FilesSizeCalculator {
8+
9+
/**
10+
* Returns the cummulative size of all the files
11+
*/
12+
public getFilesSize(filePaths: string[] | undefined) : number {
13+
var totalSize = 0;
14+
15+
if (!util.isNullOrUndefined(filePaths)) {
16+
filePaths.forEach(filePath => {
17+
totalSize += this.getFileSize(filePath);
18+
});
19+
}
20+
21+
return totalSize;
22+
}
23+
24+
public getFileSize(filePath: string) : number {
25+
const stats = fs.statSync(filePath);
26+
return stats["size"];
27+
}
28+
29+
}

src/apkAnalyzer/MetaMfParser.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import LineReader from 'line-reader';
2+
import * as path from 'path';
3+
import * as util from 'util';
4+
5+
/**
6+
* Map from extension to all files with that extension
7+
*/
8+
interface ExtensionFiles {
9+
[key: string]: Array<string>;
10+
}
11+
12+
/**
13+
* Parses the contents of the Android MetaMf file and sorts the list of files based on extension
14+
*/
15+
export default class MetaMfParser {
16+
17+
// Path which is root of the apk contents
18+
mBasePath: string;
19+
20+
// Path to the actual .MF file
21+
mfFilePath: string;
22+
23+
mFilesExtensionMap: Map<string, Array<string>>;
24+
25+
constructor(apkContentsPath: string) {
26+
this.mBasePath = apkContentsPath;
27+
this.mfFilePath = path.join(apkContentsPath, 'META-INF', 'MANIFEST.MF');
28+
this.mFilesExtensionMap = new Map();
29+
}
30+
31+
/**
32+
* Parses the file contents into extension groups
33+
*/
34+
public async parse() {
35+
var eachLine: any = util.promisify(LineReader.eachLine);
36+
return eachLine(this.mfFilePath, (line: string) => {
37+
this.parseLine(line)
38+
});
39+
}
40+
41+
/**
42+
* Gets array of files with the given extension
43+
*/
44+
public getFiles(extension: string) : Array<string> | undefined {
45+
return this.mFilesExtensionMap.get(extension);
46+
}
47+
48+
private parseLine(line: string) {
49+
// Ignore lines which doesn't point to a file
50+
if (!line.startsWith('Name: ')) {
51+
return;
52+
}
53+
54+
const filePath = path.join(this.mBasePath, line.split('Name: ', 2)[1]);
55+
const extension = path.extname(filePath);
56+
57+
if (!this.mFilesExtensionMap.has(extension)) {
58+
this.mFilesExtensionMap.set(extension, []);
59+
}
60+
61+
this.mFilesExtensionMap.get(extension)?.push(filePath);
62+
}
63+
}

src/task.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@
2929
"defaultValue": "",
3030
"required": true,
3131
"helpMarkDown": "Path to target apk / aab. This is the app after changes"
32+
},
33+
{
34+
"name": "summaryOutputPath",
35+
"type": "string",
36+
"label": "Summary output path",
37+
"defaultValue": "summary.md",
38+
"required": true,
39+
"helpMarkDown": "Output file where comparision summary should be written to"
3240
}
3341
],
3442
"execution": {

0 commit comments

Comments
 (0)