Skip to content

Commit 2e28f2e

Browse files
authored
[apkdiff] Add apk diff tool (#4221)
Fixes: #4191 Context: https://github.com/xamarin/xamarin-android/projects/14 Add `tools/apkdiff`, a tool for comparing `.apk` files. It has three modes of operation: 1. Compare two `.apk` files: apkdiff App1.apk App2.apk 2. Create a `.apkdesc` file from a `.apk`, which is a JSON file listing all the entries in the `.apk` and their sizes: apkdiff -s App1.apk App1.apkdesc 3. Compare a `.apk` file to an `.apkdesc` file: apkdiff App1.apk App1.apkdesc The `.apkdesc` JSON files will eventually replace the `.csv` files within `tests/apk-sizes-reference` (e8b9ee2), and look like: { "Comment": "HEAD/master: cdc04224edcb876ae6607693ea4011fee8c76893", "PackageSize": 75817581, "Entries": { "AndroidManifest.xml": { "Size": 5428 }, "res/drawable/android_button.xml": { "Size": 588 }, ... } Example "diff" output: $ mono apkdiff.exe xa-d16-4/bin/TestRelease/Xamarin.Forms_Performance_Integration.apkdesc xa-d16-5/bin/TestRelease/Xamarin.Forms_Performance_Integration.apk Size difference in bytes ([*1] apk1 only, [*2] apk2 only): + 49184 lib/armeabi-v7a/libmonosgen-2.0.so + 13824 assemblies/Mono.Android.dll + 10824 lib/x86/libmonodroid.so + 5604 lib/armeabi-v7a/libmonodroid.so + 1864 lib/armeabi-v7a/libxamarin-app.so + 1864 lib/x86/libxamarin-app.so + 168 classes.dex - 3584 assemblies/System.dll - 10240 assemblies/mscorlib.dll - 71680 assemblies/Mono.Security.dll - 77792 lib/x86/libmonosgen-2.0.so Summary: - 46984 Package size difference
1 parent 1273970 commit 2e28f2e

File tree

13 files changed

+422
-8
lines changed

13 files changed

+422
-8
lines changed

Xamarin.Android.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "jnienv-gen", "external\Java
129129
EndProject
130130
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "check-boot-times", "build-tools\check-boot-times\check-boot-times.csproj", "{D28957BF-5E66-4D60-B528-22820C60AC82}"
131131
EndProject
132+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "apkdiff", "tools\apkdiff\apkdiff.csproj", "{4E0D89AC-1C8A-45A8-94F0-A54D1B68BE9C}"
133+
EndProject
132134
Global
133135
GlobalSection(SharedMSBuildProjectFiles) = preSolution
134136
src\Xamarin.Android.NamingCustomAttributes\Xamarin.Android.NamingCustomAttributes.projitems*{3f1f2f50-af1a-4a5a-bedb-193372f068d7}*SharedItemsImports = 4
@@ -368,6 +370,10 @@ Global
368370
{D28957BF-5E66-4D60-B528-22820C60AC82}.Debug|AnyCPU.Build.0 = Debug|Any CPU
369371
{D28957BF-5E66-4D60-B528-22820C60AC82}.Release|AnyCPU.ActiveCfg = Release|Any CPU
370372
{D28957BF-5E66-4D60-B528-22820C60AC82}.Release|AnyCPU.Build.0 = Release|Any CPU
373+
{4E0D89AC-1C8A-45A8-94F0-A54D1B68BE9C}.Debug|AnyCPU.ActiveCfg = Debug|anycpu
374+
{4E0D89AC-1C8A-45A8-94F0-A54D1B68BE9C}.Debug|AnyCPU.Build.0 = Debug|anycpu
375+
{4E0D89AC-1C8A-45A8-94F0-A54D1B68BE9C}.Release|AnyCPU.ActiveCfg = Release|anycpu
376+
{4E0D89AC-1C8A-45A8-94F0-A54D1B68BE9C}.Release|AnyCPU.Build.0 = Release|anycpu
371377
EndGlobalSection
372378
GlobalSection(SolutionProperties) = preSolution
373379
HideSolutionNode = FALSE
@@ -431,6 +437,7 @@ Global
431437
{DE40756E-57F6-4AF2-B155-55E3A88CCED8} = {05C3B1D6-A4CE-4534-A9E4-E9117591ADF7}
432438
{6410DA0F-5E14-4FC0-9AEE-F4C542C96C7A} = {05C3B1D6-A4CE-4534-A9E4-E9117591ADF7}
433439
{D28957BF-5E66-4D60-B528-22820C60AC82} = {E351F97D-EA4F-4E7F-AAA0-8EBB1F2A4A62}
440+
{4E0D89AC-1C8A-45A8-94F0-A54D1B68BE9C} = {864062D3-A415-4A6F-9324-5820237BA058}
434441
EndGlobalSection
435442
GlobalSection(ExtensibilityGlobals) = postSolution
436443
SolutionGuid = {53A1F287-EFB2-4D97-A4BB-4A5E145613F6}

build-tools/Xamarin.Android.Tools.BootstrapTasks/result-packaging.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
</ItemGroup>
3131
<ItemGroup>
3232
<_TestResultFiles Include="$(XamarinAndroidSourcePath)TestResult-*.xml" />
33+
<_TestResultFiles Include="$(XamarinAndroidSourcePath)bin\Test$(Configuration)\*.apkdesc" />
3334
<_TestResultFiles Include="$(XamarinAndroidSourcePath)bin\Test$(Configuration)\TestResult-*.xml" />
3435
<_TestResultFiles Include="$(XamarinAndroidSourcePath)bin\Test$(Configuration)\compatibility\*" />
3536
<_TestResultFiles Include="$(XamarinAndroidSourcePath)bin\Test$(Configuration)\logcat*" />

build-tools/installers/create-installers.targets

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,8 @@
102102
</ItemGroup>
103103
<ItemGroup>
104104
<_MSBuildFiles Include="$(MSBuildSrcDir)\android-support-multidex.jar" />
105+
<_MSBuildFiles Include="$(MSBuildSrcDir)\apkdiff.exe" />
106+
<_MSBuildFiles Include="$(MSBuildSrcDir)\apkdiff.pdb" />
105107
<_MSBuildFiles Include="$(MSBuildSrcDir)\aprofutil.exe" />
106108
<_MSBuildFiles Include="$(MSBuildSrcDir)\aprofutil.pdb" />
107109
<_MSBuildFiles Include="$(MSBuildSrcDir)\cil-strip.exe" />
@@ -157,6 +159,8 @@
157159
<_MSBuildFiles Include="$(MSBuildSrcDir)\Mono.Posix.NETStandard.dll" />
158160
<_MSBuildFiles Include="$(MSBuildSrcDir)\Mono.Profiler.Log.dll" />
159161
<_MSBuildFiles Include="$(MSBuildSrcDir)\Mono.Profiler.Log.pdb" />
162+
<_MSBuildFiles Include="$(MSBuildSrcDir)\Newtonsoft.Json.dll" />
163+
<_MSBuildFiles Include="$(MSBuildSrcDir)\Newtonsoft.Json-LICENSE.md" />
160164
<_MSBuildFiles Include="$(MSBuildSrcDir)\logcat-parse.exe" />
161165
<_MSBuildFiles Include="$(MSBuildSrcDir)\logcat-parse.pdb" />
162166
<_MSBuildFiles Include="$(MSBuildSrcDir)\mdoc.exe" />
@@ -286,6 +290,7 @@
286290
<_MSBuildFilesUnixSwab Include="$(MSBuildSrcDir)\$(HostOS)\ndk\x86_64-linux-android-as" />
287291
<_MSBuildFilesUnixSwab Include="$(MSBuildSrcDir)\$(HostOS)\ndk\x86_64-linux-android-ld" />
288292
<_MSBuildFilesUnixSwab Include="$(MSBuildSrcDir)\$(HostOS)\ndk\x86_64-linux-android-strip" />
293+
<_MSBuildFilesUnix Include="$(MSBuildSrcDir)\$(HostOS)\apkdiff" />
289294
<_MSBuildFilesUnix Include="$(MSBuildSrcDir)\$(HostOS)\illinkanalyzer" />
290295
<_MSBuildFilesUnix Include="$(MSBuildSrcDir)\$(HostOS)\jit-times" />
291296
<_MSBuildFilesUnix Include="$(MSBuildSrcDir)\$(HostOS)\aprofutil" />

build-tools/scripts/TestApks.targets

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,15 @@
347347
LabelSuffix="-$(Configuration)$(TestsFlavor)"
348348
ContinueOnError="ErrorAndContinue"
349349
/>
350+
<PropertyGroup>
351+
<ApkDiffPath>$(XAInstallPrefix)xbuild\Xamarin\Android\$(HostOS)\apkdiff</ApkDiffPath>
352+
<ApkDiffPath Condition=" !Exists('$(ApkDiffPath)') ">$(MonoAndroidBinDirectory)\apkdiff</ApkDiffPath>
353+
</PropertyGroup>
354+
<Exec
355+
Condition=" Exists('$(ApkDiffPath)') "
356+
Command="&quot;$(ApkDiffPath)&quot; -v -c &quot;HEAD/`git branch --show-current`: `git rev-parse HEAD`&quot; -s &quot;%(_AllArchives.Identity)&quot;;mv &quot;%(_AllArchives.RelativeDir)\%(_AllArchives.Filename).apkdesc&quot; &quot;%(_AllArchives.RelativeDir)\%(_AllArchives.Filename)-$(Configuration)$(TestsFlavor).apkdesc&quot;"
357+
ContinueOnError="ErrorAndContinue"
358+
/>
350359
</Target>
351360
<Target Name="CheckBootTimes"
352361
DependsOnTargets="AcquireAndroidTarget;ReleaseAndroidTarget">

build-tools/xaprepare/xaprepare/Steps/Step_PrepareLocal.cs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,28 @@ public Step_PrepareLocal ()
1010
: base ("Preparing local components")
1111
{}
1212

13-
protected override async Task<bool> Execute(Context context)
13+
async Task<bool> Restore (MSBuildRunner msbuild, string csprojPath, string logTag, string binLogName)
1414
{
15-
var msbuild = new MSBuildRunner (context);
16-
17-
string xfTestPath = Path.Combine (BuildPaths.XamarinAndroidSourceRoot, "tests", "Xamarin.Forms-Performance-Integration", "Xamarin.Forms.Performance.Integration.csproj");
1815
return await msbuild.Run (
19-
projectPath: xfTestPath,
20-
logTag: "xfperf",
21-
arguments: new List <string> {
16+
projectPath: csprojPath,
17+
logTag: logTag,
18+
arguments: new List<string> {
2219
"/t:Restore"
2320
},
24-
binlogName: "prepare-restore"
21+
binlogName: binLogName
2522
);
2623
}
24+
25+
protected override async Task<bool> Execute(Context context)
26+
{
27+
var msbuild = new MSBuildRunner (context);
28+
29+
string xfTestPath = Path.Combine (BuildPaths.XamarinAndroidSourceRoot, "tests", "Xamarin.Forms-Performance-Integration", "Xamarin.Forms.Performance.Integration.csproj");
30+
if (!await Restore (msbuild, xfTestPath, "xfperf", "prepare-restore"))
31+
return false;
32+
33+
var apkDiffPath = Path.Combine (BuildPaths.XamarinAndroidSourceRoot, "tools", "apkdiff", "apkdiff.csproj");
34+
return await Restore (msbuild, apkDiffPath, "apkdiff", "prepare-restore-apkdiff");
35+
}
2736
}
2837
}

src/Xamarin.Android.Build.Tasks/Xamarin.Android.Build.Tasks.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,7 @@
274274
<_MonoScript Include="illinkanalyzer" />
275275
<_MonoScript Include="jit-times" />
276276
<_MonoScript Include="aprofutil" />
277+
<_MonoScript Include="apkdiff" />
277278
<_MonoScriptSource Include="@(_MonoScript->'$(_MonoScriptSourceDirectory)\%(Identity)')" />
278279
<_MonoScriptSource Include="mono.config" />
279280
<_MonoScriptDestination Include="@(_MonoScript->'$(_MonoScriptDestinationDirectory)\%(Identity)')" />

tools/apkdiff/ApkDescription.cs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
using System;
2+
using System.IO;
3+
using System.Linq;
4+
using System.Collections.Generic;
5+
6+
using System.Runtime.Serialization;
7+
using Xamarin.Tools.Zip;
8+
9+
namespace apkdiff {
10+
11+
struct FileInfo {
12+
public long Size;
13+
}
14+
15+
[DataContract (Namespace = "apk")]
16+
public class ApkDescription {
17+
18+
[DataMember]
19+
string Comment;
20+
21+
[DataMember]
22+
long PackageSize;
23+
string PackagePath;
24+
25+
[DataMember]
26+
readonly Dictionary<string, FileInfo> Entries = new Dictionary<string, FileInfo> ();
27+
28+
public static ApkDescription Load (string path)
29+
{
30+
if (!File.Exists (path)) {
31+
Program.Error ($"File '{path}' does not exist.");
32+
Environment.Exit (2);
33+
}
34+
35+
var extension = Path.GetExtension (path);
36+
switch (extension.ToLower ()) {
37+
case ".apk":
38+
var nd = new ApkDescription ();
39+
40+
nd.LoadApk (path);
41+
42+
return nd;
43+
case ".apkdesc":
44+
return LoadDescription (path);
45+
default:
46+
Program.Error ($"Unknown file extension '{extension}'");
47+
Environment.Exit (3);
48+
49+
return null;
50+
}
51+
}
52+
53+
void LoadApk (string path)
54+
{
55+
var zip = ZipArchive.Open (path, FileMode.Open);
56+
57+
if (Program.Verbose)
58+
Program.ColorWriteLine ($"Loading apk '{path}'", ConsoleColor.Yellow);
59+
60+
PackageSize = new System.IO.FileInfo (path).Length;
61+
PackagePath = path;
62+
63+
foreach (var entry in zip) {
64+
var name = entry.FullName;
65+
66+
if (Entries.ContainsKey (name)) {
67+
Program.Warning ("Duplicate APK file entry: {name}");
68+
continue;
69+
}
70+
71+
Entries [name] = new FileInfo { Size = (long)entry.Size };
72+
73+
if (Program.Verbose)
74+
Program.ColorWriteLine ($" {entry.Size,12} {name}", ConsoleColor.Gray);
75+
}
76+
77+
if (Program.SaveDescriptions) {
78+
var descPath = Path.ChangeExtension (path, ".apkdesc");
79+
80+
Program.ColorWriteLine ($"Saving apk description to '{descPath}'", ConsoleColor.Yellow);
81+
SaveDescription (descPath);
82+
}
83+
}
84+
85+
static ApkDescription LoadDescription (string path)
86+
{
87+
if (Program.Verbose)
88+
Program.ColorWriteLine ($"Loading description '{path}'", ConsoleColor.Yellow);
89+
90+
using (var reader = File.OpenText (path)) {
91+
return new Newtonsoft.Json.JsonSerializer ().Deserialize (reader, typeof (ApkDescription)) as ApkDescription;
92+
}
93+
}
94+
95+
void SaveDescription (string path)
96+
{
97+
Comment = Program.Comment;
98+
99+
using (var writer = File.CreateText (path)) {
100+
new Newtonsoft.Json.JsonSerializer () { Formatting = Newtonsoft.Json.Formatting.Indented }.Serialize (writer, this);
101+
}
102+
}
103+
104+
void PrintDifference (string key, long diff, string comment = null)
105+
{
106+
var color = diff > 0 ? ConsoleColor.Red : ConsoleColor.Green;
107+
Program.ColorWrite ($" {diff:+;-;+}{Math.Abs (diff),12}", color);
108+
Program.ColorWrite ($" {key}", ConsoleColor.Gray);
109+
Program.ColorWriteLine (comment, color);
110+
}
111+
112+
public void Compare (ApkDescription other)
113+
{
114+
var keys = Entries.Keys.Union (other.Entries.Keys);
115+
var differences = new Dictionary<string, long> ();
116+
var singles = new HashSet<string> ();
117+
118+
Program.ColorWriteLine ("Size difference in bytes ([*1] apk1 only, [*2] apk2 only):", ConsoleColor.Yellow);
119+
120+
foreach (var key in Entries.Keys) {
121+
if (other.Entries.ContainsKey (key)) {
122+
differences [key] = other.Entries [key].Size - Entries [key].Size;
123+
} else {
124+
differences [key] = -Entries [key].Size;
125+
singles.Add (key);
126+
}
127+
}
128+
129+
foreach (var key in other.Entries.Keys) {
130+
if (Entries.ContainsKey (key))
131+
continue;
132+
133+
differences [key] = other.Entries [key].Size;
134+
singles.Add (key);
135+
}
136+
137+
foreach (var diff in differences.OrderByDescending (v => v.Value)) {
138+
if (diff.Value == 0)
139+
continue;
140+
141+
PrintDifference (diff.Key, diff.Value, singles.Contains (diff.Key) ? $" *{(diff.Value > 0 ? 2 : 1)}" : null);
142+
}
143+
144+
Program.ColorWriteLine ("Summary:", ConsoleColor.Green);
145+
if (Program.Verbose)
146+
Program.ColorWriteLine ($" apk1: {PackageSize,12} {PackagePath}\n apk2: {other.PackageSize,12} {other.PackagePath}", ConsoleColor.Gray);
147+
148+
PrintDifference ("Package size difference", other.PackageSize - PackageSize);
149+
}
150+
}
151+
}

tools/apkdiff/Program.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using System;
2+
using System.IO;
3+
using Mono.Options;
4+
5+
using static System.Console;
6+
7+
namespace apkdiff {
8+
class Program {
9+
static readonly string Name = "apkdiff";
10+
11+
public static string Comment;
12+
public static bool SaveDescriptions;
13+
public static bool Verbose;
14+
15+
public static void Main (string [] args)
16+
{
17+
var (path1, path2) = ProcessArguments (args);
18+
19+
var desc1 = ApkDescription.Load (path1);
20+
21+
if (path2 != null) {
22+
var desc2 = ApkDescription.Load (path2);
23+
24+
desc1.Compare (desc2);
25+
}
26+
}
27+
28+
static (string, string) ProcessArguments (string [] args)
29+
{
30+
var help = false;
31+
var options = new OptionSet {
32+
$"Usage: {Name}.exe OPTIONS* <package1.apk[desc]> [<package2.apk[desc]>]",
33+
"",
34+
"Compares APK packages content or APK package with content description",
35+
"",
36+
"Copyright 2020 Microsoft Corporation",
37+
"",
38+
"Options:",
39+
{ "c|comment=",
40+
"Comment to be saved inside .apkdesc file",
41+
v => Comment = v },
42+
{ "h|help|?",
43+
"Show this message and exit",
44+
v => help = v != null },
45+
{ "s|save-descriptions",
46+
"Save .apkdesc files next to the apk package(s)",
47+
v => SaveDescriptions = true },
48+
{ "v|verbose",
49+
"Output information about progress during the run of the tool",
50+
v => Verbose = true },
51+
};
52+
53+
var remaining = options.Parse (args);
54+
55+
if (help || args.Length < 1) {
56+
options.WriteOptionDescriptions (Out);
57+
58+
Environment.Exit (0);
59+
}
60+
61+
if (remaining.Count != 2 && (remaining.Count != 1 || !SaveDescriptions)) {
62+
Error ("Please specify 2 APK packages to compare or 1 and use -s option.");
63+
Environment.Exit (1);
64+
}
65+
66+
return (remaining [0], remaining.Count > 1 ? remaining [1] : null);
67+
}
68+
69+
static void ColorMessage (string message, ConsoleColor color, TextWriter writer, bool writeLine = true)
70+
{
71+
ForegroundColor = color;
72+
73+
if (writeLine)
74+
writer.WriteLine (message);
75+
else
76+
writer.Write (message);
77+
78+
ResetColor ();
79+
}
80+
81+
public static void ColorWriteLine (string message, ConsoleColor color) => ColorMessage (message, color, Out);
82+
83+
public static void ColorWrite (string message, ConsoleColor color) => ColorMessage (message, color, Out, false);
84+
85+
public static void Error (string message) => ColorMessage ($"Error: {Name}: {message}", ConsoleColor.Red, Console.Error);
86+
87+
public static void Warning (string message) => ColorMessage ($"Warning: {Name}: {message}", ConsoleColor.Yellow, Console.Error);
88+
}
89+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
4+
// Information about this assembly is defined by the following attributes.
5+
// Change them to the values specific to your project.
6+
7+
[assembly: AssemblyTitle ("apkdiff")]
8+
[assembly: AssemblyDescription ("")]
9+
[assembly: AssemblyConfiguration ("")]
10+
[assembly: AssemblyCompany ("Microsoft Corporation")]
11+
[assembly: AssemblyProduct ("")]
12+
[assembly: AssemblyCopyright ("2020 Microsoft Corporation")]
13+
[assembly: AssemblyTrademark ("")]
14+
[assembly: AssemblyCulture ("")]
15+
16+
// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}".
17+
// The form "{Major}.{Minor}.*" will automatically update the build and revision,
18+
// and "{Major}.{Minor}.{Build}.*" will update just the revision.
19+
20+
[assembly: AssemblyVersion ("0.0.1")]
21+
22+
// The following attributes are used to specify the signing key for the assembly,
23+
// if desired. See the Mono documentation for more information about signing.
24+
25+
//[assembly: AssemblyDelaySign(false)]
26+
//[assembly: AssemblyKeyFile("")]

0 commit comments

Comments
 (0)