#!/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 () { 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 () { 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 IDs system("$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 IDs open(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 .");