#!/bin/perl
use strict;use warnings;
my $ANDROID_VERSION;
my $SDK_DIR;my $TOOLS_DIR;my $PLATFORM_DIR;
my $CMD_DELETE;my $CMD_JAR;my $CMD_JAVAC;my $CMD_JAVA;my $CMD_D8;
my $LIB_RES_DIR;my $LIB_CLASS_DIR;
# get variables from includes.sh{ open(my $FILE, '<', "includes.sh");
foreach my $line (<$FILE>) { if (length($line) < 2 or substr($line, 0, 1) eq '#') { next; } my $decl = substr($line, 0, -1); $decl =~ s/="/ = "/; $decl =~ s/='/ = '/; $decl = "\$" . $decl . ";\n"; eval($decl); }
close($FILE);}
# Every library/package that uses resources needs a list that maps resource variables to IDs in code form.# It starts with a simple text file that gets translated into a .java, which is in turn compiled into the package.# All that's needed is a simple re-formatting for each line, with a few Java keywords and curly braces in-between.
sub gen_rjava { my $pkg = shift; my $r_txt = shift;
my @out = ( "// Auto-generated by an unofficial tool", "", "package $pkg;", "", "public final class R {" );
my $class = "";
foreach my $line (@$r_txt) { my @info = split(/ /, $line, 4); $info[1] =~ s/[^0-9a-zA-Z]/_/;
my $colon = rindex($info[2], ':'); if ($colon >= 0) { $info[2] = substr($info[2], $colon + 1); }
if ($info[1] ne $class) { push(@out, "\t}") if (length($class) > 0);
$class = $info[1]; push(@out, "\tpublic static final class $class {"); }
push(@out, "\t\tpublic static final ${info[0]} ${info[2]}=${info[3]};"); }
push(@out, ("\t}", "}", "")); return \@out;}
# The only reason this script cares about the AndroidManifest.xml file (found inside every APK and AAR)# is so that it can consistently find the name of the package, which is what this function does
sub get_package_from_manifest { my $path = shift;
if (!-f $path) { print("Could not find manifest file $path"); return undef; }
open(my $fh, '<', $path); read($fh, my $manifest, -s $fh); close($fh);
if ($manifest =~ /package=["']([^"']+)/g) { return $1; } else { print("Could not find a suitable package name inside AndroidManifest.xml\n"); }
return undef;}
# This function creates an R.txt file based on the resources in the 'res' folder of the main project,# which is later turned into R.java and finally R.jar.# Essentially, each values XML is scanned for strings or other values defined on a particular line,# and everything else is just scanned for its file name.# The ID that each resource is given here is unimportant other than that it needs to be unique merely within this generated document.# All resource IDs (including these ones) get overwritten later.
sub gen_proj_rtxt { my @r_txt = (); my $type_idx = 0;
# The meaning of "id" here is a type, not a number that is used to identify a resource # We create a separate array and push it to the main r_txt at the end to work around the limitations of our gen_rjava() implementation. my @layout_id_defs = ();
foreach my $dir (<res/*>) { my $sub_idx = 0;
# If this type-folder is a values folder if ($dir =~ /values/) { foreach my $f (<$dir/*.xml>) { open(my $fh, '<', $f); foreach (<$fh>) { if ($_ =~ /<([^ ]+).+name="([^"]+)"/) { my $id = sprintf('0x7f%02x%04x', $type_idx, $sub_idx); push(@r_txt, "int $1 $2 $id"); $sub_idx++; } } close($fh); } } else { # 4 == length("res/") my $type = substr($dir, 4); my $len = length($dir) + 1;
foreach my $f (<$dir/*>) { # Some XML files (especially layout files) contain resource definitions under the type "id" if (substr($f, -4) eq ".xml") { open(my $fh, '<', $f); foreach (<$fh>) { if ($_ =~ /@\+id\/([^"]+)/) { my $id = sprintf('0x7f%02x%04x', $type_idx, $sub_idx); push(@layout_id_defs, "int id $1 $id"); $sub_idx++; } } close($fh); }
my $dot_idx = index($f, ".", $len); my $name = ($dot_idx < 0) ? substr($f, $len) : substr($f, $len, $dot_idx - $len);
my $id = sprintf('0x7f%02x%04x', $type_idx, $sub_idx); push(@r_txt, "int $type $name $id"); $sub_idx++; } }
$type_idx++; }
push(@r_txt, @layout_id_defs);
open(my $fh, '>', "build/R.txt"); print $fh join("\n", @r_txt); close($fh);}
# This is the big one. This is where all resource IDs get overwritten.# When AAPT2 links all resources from all the libraries (and the main project) together, it reallocates all IDs so that they are unique.# We take the new list of IDs and apply it to each package's resource listing, which AAPT2 doesn't do for us.# After this, there should be no resource collisions at app runtime.
sub update_res_ids { my $ids = shift; my $r_list = shift;
my @table = (); # list of offsets to the provided files inside 'blob' my $fmt = ""; # format string to pack the list of files into a single blob my @files = ();
# Load each R.txt my $size = 0; foreach (@$r_list) { open(my $fh, '<:raw', $_); my $s = -s $fh; read($fh, my $r, $s); close($fh);
push(@table, $size); $fmt .= "a$s "; push(@files, $r); $size += $s; } push(@table, $size);
# Make a copy of each R.txt and embed it into one homogenous string. This is likely faster than scanning each file individually. my $blob = pack($fmt, @files);
# Make an index of replacements to happen later. This means there (shouldn't) be any search-replace ordering issues. my @repl_list = ();
# For each line in the AAPT2 'ids.txt' output foreach (@$ids) { # Hard-coded 10 == length("0xnnnnnnnn"), the 32-bit hex number scheme that IDs use in text form my $new_id = substr($_, -10);
my $nm_start = index($_, ':') + 1; my $nm_end = index($_, ' ') + 1;
# 'name' will look like "type variable " my $name = substr($_, $nm_start, $nm_end - $nm_start); $name =~ s/\// /;
# For each instance where 'name' gets defined as a single ID: my $name_reg = qr/$name(0x[0-9a-fA-F]+)/; while ($blob =~ /$name_reg/g) { my $match_len = length($1); my $off = (pos $blob) - $match_len; next if ($off < 0);
# If the ID is not a complete ID (likely 0x0), just mark a single replacement if ($match_len != 10) { push(@repl_list, {"off" => $off, "len" => $match_len, "new" => $new_id}); next; }
# Find the current file my $file_idx = 0; $file_idx++ while ($table[$file_idx] < $off); $file_idx--;
# Find all instances of the old ID for this new ID so we can replace them all my $id_reg = qr/$1/; while ($files[$file_idx] =~ /$id_reg/g) { my $pos = (pos $files[$file_idx]) - $match_len; next if ($pos < 0);
push(@repl_list, {"off" => $pos + $table[$file_idx], "len" => 10, "new" => $new_id}); } } }
# Since @ids (from ids.txt by AAPT2 link) is not sorted in a convenient order, we sort the replacement list here # so that for each offset, the necessary displacement can be calculated linearly my @replacements = sort { $a->{"off"} <=> $b->{"off"} } @repl_list;
my $n_repl = @replacements; my $file_idx = 0; my $disp = 0;
# This assumes that at least one replacement is needed in each file for (my $i = 0; $i < $n_repl; $i++) { my $repl = $replacements[$i]; my $off = $repl->{"off"}; my $len = $repl->{"len"};
# We need to update the table of file offsets so that we can write the correct range of bytes to the intended file later while ($off > $table[$file_idx + 1]) { $file_idx++; $table[$file_idx] += $disp; }
# Actually replace the old ID with the new one. # The key here is that the new ID may not necessarily be the same length as the old one, # so everything after this replacement may get shifted up/down. # A displacement is calculated as we go so that the current offset is always up to date.
substr($blob, $off + $disp, $len) = $repl->{"new"}; $disp += 10 - $len; # disp += length($repl->{"new"}) - $len }
# Make sure the last file has its size corrected as well $table[-1] += $disp;
# Overwrite all the R.txts my $idx = 0; foreach (@$r_list) { open(my $fh, '>', $_); my $len = $table[$idx+1] - $table[$idx]; print $fh substr($blob, $table[$idx], $len); close($fh); $idx++; }}
# This function iterates over every AAR, finds its R.txt, generates R.java and compiles it,# placing the resulting .class files inside the already extracted classes folder for the current package.# This means when the library is properly compiled into a JAR later, it knows how to access its own resources.
sub gen_libs_rjava { my $dir = "$LIB_RES_DIR/r_java"; my @rjava_list = ();
foreach (<lib/*.aar>) { my $name = substr($_, 4, -4);
my $in_path = "$LIB_RES_DIR/${name}_R.txt";
if (!-f $in_path) { print("No resources file for $name, skipping...\n"); next; }
open(my $fh, '<', $in_path); chomp(my @r_txt = <$fh>); close($fh);
# skip this library if the resources index is empty if (@r_txt <= 0) { print("R.txt for $name is missing, skipping...\n"); next; }
my $package = get_package_from_manifest("$LIB_RES_DIR/${name}_mf.xml"); if (!defined($package)) { # a bit harsh ;) print("Could not find package name inside ${name}/AndroidManifest.xml, skipping...\n"); next; }
my $out_path = "$dir/$name"; mkdir($out_path) if (!-d $out_path);
my $r_java = gen_rjava($package, \@r_txt);
$out_path .= "/R.java"; push(@rjava_list, $out_path);
open($fh, '>', $out_path); print $fh join("\n", @$r_java); close($fh); }
return \@rjava_list;}
# Entry-point
if (-d "lib" && not (-d $LIB_RES_DIR && -d $LIB_CLASS_DIR)) { print( "This stage depends on library resources already being compiled.\n", "Run export-libs.pl first.\n" ); exit;}
mkdir("build") if (!-d "build");
my $aapt2_res = "build/res.zip";
if (-d "lib") { print("Compiling library resources...\n");
system("$TOOLS_DIR/aapt2 compile -o build/res_libs.zip --dir lib/res/res"); exit if ($? != 0);
$aapt2_res = "build/res_libs.zip " . $aapt2_res;}
print("Compiling project resources...\n");
system("$TOOLS_DIR/aapt2 compile -o build/res.zip --dir res");exit if ($? != 0);
print("Linking resources...\n");
# This is what gives us the actual set of properly unique IDssystem("$TOOLS_DIR/aapt2 link -o build/unaligned.apk --manifest AndroidManifest.xml -I $PLATFORM_DIR/android.jar --emit-ids ids.txt $aapt2_res");exit if ($? != 0);
# Load those unique IDsopen(my $fh, '<', "ids.txt");chomp(my @ids = <$fh>);close($fh);
system("$CMD_DELETE ids.txt");
print("Generating project R.txt...\n");
gen_proj_rtxt();
print("Updating resource IDs...\n");
my @r_list = ("build/R.txt");push(@r_list, <$LIB_RES_DIR/*_R.txt>);
update_res_ids(\@ids, \@r_list);
if (-d $LIB_RES_DIR && -d $LIB_CLASS_DIR) { print("Generating library resource maps...\n"); mkdir("$LIB_RES_DIR/r_java") if (!-d "$LIB_RES_DIR/r_java"); my $rjava_list = gen_libs_rjava();
open(my $fh, '>', "rjava_list.txt"); print $fh join("\n", @$rjava_list); close($fh);
print("Compiling resource maps...\n"); mkdir("$LIB_RES_DIR/R") if (!-d "$LIB_RES_DIR/R");
system("$CMD_JAVAC -source 8 -target 8 -bootclasspath $PLATFORM_DIR/android.jar -d $LIB_RES_DIR/R \@rjava_list.txt"); exit if ($? != 0); unlink("rjava_list.txt");
system("$CMD_JAR --create --file build/libs_r.jar -C '$LIB_RES_DIR/R' ."); exit if ($? != 0);
print("Compiling resource maps into DEX bytecode...\n"); system("$CMD_D8 --intermediate build/libs_r.jar --classpath $PLATFORM_DIR/android.jar --output build"); exit if ($? != 0); rename("build/classes.dex", "build/libs_r.dex");
print("Fusing library classes into a .JAR...\n"); system("$CMD_JAR --create --file build/libs.jar -C '$LIB_CLASS_DIR' ."); exit if ($? != 0);
print("Compiling library .JAR into DEX bytecode...\n"); system("$CMD_D8 --intermediate build/libs.jar --classpath $PLATFORM_DIR/android.jar --output build"); exit if ($? != 0); rename("build/classes.dex", "build/libs.dex");}
print("Generating project R.java...\n");
my $pkg = get_package_from_manifest("AndroidManifest.xml");exit if (!defined($pkg));
open($fh, '<', "build/R.txt");chomp(my @r_txt = <$fh>);close($fh);
my $r_java = gen_rjava($pkg, \@r_txt);
open($fh, '>', "build/R.java");print $fh join("\n", @$r_java);close($fh);
print("Compiling project R.java...\n");
mkdir("build/R") if (!-d "build/R");
system("$CMD_JAVAC -source 8 -target 8 -bootclasspath $PLATFORM_DIR/android.jar build/R.java -d build/R");exit if ($? != 0);
system("$CMD_JAR --create --file build/R.jar -C build/R .");