Feature Request: Android App Bundles

I handles .layout-xml fine though. --legacy doesnt seem to help for this file.

Hmm, pretty sure the structure is what it’s supposed to be, but I guess I’ll compare it against the docs in detail…

Ah, my mistake. I changed my layout xmls back to layout-xml but did not change my string back to android-xml, so when it worked for me it was with strings.xml. I’m seeing the same issue now with strings.android-xml

Yeah, looks like we’ll need to rename those. I’ll handle.

1 Like
E:                   aapt: invalid file path '/Users/mh/Library/Application Support/RemObjects Software/EBuild/Obj/com.remobjects.androidapplication45-B5CB0D4D7B3898F4CC11143E27D750E3B3F7544B/Release/Cooper-Android/temp-res/main.xml'.
E:                   aapt: invalid file path '/Users/mh/Library/Application Support/RemObjects Software/EBuild/Obj/com.remobjects.androidapplication45-B5CB0D4D7B3898F4CC11143E27D750E3B3F7544B/Release/Cooper-Android/temp-res/strings.xml'.

damn :(. it really needs them in the right folder structure, too.

Yeah, fwiw here’s the aapt2 source code for this bit:

static std::string buildIntermediateFilename(const std::string outDir,
                                             const ResourcePathData& data) {
    std::stringstream name;
    name << data.resourceDir;
    if (!data.configStr.empty()) {
        name << "-" << data.configStr;
    }
    name << "_" << data.name << "." << data.extension << ".flat";
    std::string outPath = outDir;
    file::appendPath(&outPath, name.str());
    return outPath;
}

int compile(const std::vector<StringPiece>& args) {
	CompileOptions options;
	Maybe<std::string> product;
	Flags flags = Flags()
			.requiredFlag("-o", "Output path", &options.outputPath)
			.optionalFlag("--product", "Product type to compile", &product)
			.optionalSwitch("-v", "Enables verbose logging", &options.verbose);
	if (!flags.parse("aapt2 compile", args, &std::cerr)) {
		return 1;
	}
	if (product) {
		options.product = util::utf8ToUtf16(product.value());
	}
	CompileContext context;
	std::vector<ResourcePathData> inputData;
	inputData.reserve(flags.getArgs().size());
	// Collect data from the path for each input file.
	for (const std::string& arg : flags.getArgs()) {
		std::string errorStr;
		if (Maybe<ResourcePathData> pathData = extractResourcePathData(arg, &errorStr)) {
			inputData.push_back(std::move(pathData.value()));
		} else {
			context.getDiagnostics()->error(DiagMessage() << errorStr << " (" << arg << ")");
			return 1;
		}
	}
	bool error = false;
	for (ResourcePathData& pathData : inputData) {
		if (options.verbose) {
			context.getDiagnostics()->note(DiagMessage(pathData.source) << "processing");
		}
		if (pathData.resourceDir == u"values") {
			// Overwrite the extension.
			pathData.extension = "arsc";
			const std::string outputFilename = buildIntermediateFilename(
					options.outputPath, pathData);
			if (!compileTable(&context, options, pathData, outputFilename)) {
				error = true;
			}
		} else {
			const std::string outputFilename = buildIntermediateFilename(options.outputPath,
																		 pathData);
			if (const ResourceType* type = parseResourceType(pathData.resourceDir)) {
				if (*type != ResourceType::kRaw) {
					if (pathData.extension == "xml") {
						if (!compileXml(&context, options, pathData, outputFilename)) {
							error = true;
						}
					} else if (pathData.extension == "png" || pathData.extension == "9.png") {
						if (!compilePng(&context, options, pathData, outputFilename)) {
							error = true;
						}
					} else {
						if (!compileFile(&context, options, pathData, outputFilename)) {
							error = true;
						}
					}
				} else {
					if (!compileFile(&context, options, pathData, outputFilename)) {
						error = true;
					}
				}
			} else {
				context.getDiagnostics()->error(
						DiagMessage() << "invalid file path '" << pathData.source << "'");
				error = true;
			}
		}
	}
	if (error) {
		return 1;
	}
	return 0;
}

I’ve made these explicit errors for now, and we can add some IDE toolage to rename these files in the project, once it all works.

If you wanna try it out, I have competed my first round of changes, you can explicitly set “UseAAPT2” to true to enable it (say to see if it compile/link works with you set of files). My nest step (maybe not today) would be to figure out what needs changing in the manifest file…

1 Like

I haven’t run this manually yet, but it seems like aapt2 should work fine with old dex based on this blog post where the author walks through a complete manual build using aapt2 and dx.

The Android Developers blog also seems to indicate that d8 and dx are fully interchangeable, and that even when dx is officially deprecated it will still work with the Android toolchain.

1 Like

Just manually confirmed that it is possible to build an aab/apk with aapt2 using dx instead of d8.

1 Like

Confirmed a runtime startup crash when using D8 with multidex. The documentation on d8 is pretty limited, but here’s what I’ve found…

When MultiDex is enabled, EBuild uses the dx switch --multi-dex which the javadoc says “allows to generate several dex files if needed.” d8 does not have this switch, because it multidexes automatically when needed (I confirmed this when building in EBuild with D8).

My runtime crash is due to a NoClassDefFoundError on my class AccordanceAndroidApplication, which my manifest sets as the base Application class
<application android:name=".AccordanceAndroidApplication" ...

I did a dexdump on each of the .dex files and found that AccordanceAndroidApplication is in classes2.dex rather than classes.dex. The Multidex docs state that “If any class that’s required during startup is not provided in the primary DEX file, then your app crashes with the error java.lang.NoClassDefFoundError .”

So, it looks like EBuild needs to add the d8 switch --main-dex-list <file> when Multidex is enabled, and pass in a keep file. Here’s the docs on that switch:

–main-dex-list path

Specifies a text file that lists classes d8 should include in the main DEX file, which is typically named classes.dex . That is, when you don’t specify a list of classes using this flag, d8 does not guarentee which classes are included in the main DEX file.

Because the Android system loads the main DEX file first when starting your app, you can use this flag to prioritize certain classes at startup by compiling them into the main DEX file. This is particularly useful when supporting legacy multidex because only classes in the main DEX file are available at runtime until the legacy multidex library is loaded.

The keep file, according to the Gradle docs:

should contain one class per line, in the following format: com/example/MyClass.class

Once I get EBuild building on my machine I’ll try hardcoding a keep file in with the Application class I need to confirm that this is the solution.

Ah, so I guess when using D8, I’ll just need to always expect potentially more than one dex file to be generated, and prpovcess therm all (as I do for Multi-dev, in pre-D8). Lemme have a look at that code… (most of the android tool chain I basically “inherited” when going EBuild, so large parts of it are still a black box I merely ported, and did truly understand/review yet, though as I go thru it when changes are needed, I then do so. :wink:

Hmm. how to find what classes to keep, though? all there ones listed as Activities in the manifest?

That part is fixed.

1 Like

Gradle/Android Studio actually only provide an option to manually link a user-created file, so it relies on the developer to know which classes to keep. I suppose you could parse the manifest and automatically add any application, activities, services, etc in there, but it may be best to just let the dev add a custom txt file since they could potentially access classes through reflection, which would be near impossible to detect and would still break it if accessed too soon.

Sounds good. Re-reading that description, I understand that in “normal” cases, it should work no matter what dex file your main class is in, it just might not be as optimized as it could be. If so, that latest commit should have that fix (and I’ll add an option for a providing a custom file).

Yeah that’s a bit unclear to me. It almost sounds like it should “just work” for non-legacy mode (anything >= SDK 21) but I’m getting that crash on an sdk 29 device with a min sdk 21 project, so it does seem that even when not in legacy mode it still needs at least the Application class to be in the primary dex.

Well, the current code will only package classes.dex, so kid your class is in classes2.dex, it wont make it into the app, until you have my fix :wink:

Ah :joy: I guess I did only check the intermediate folder for dex files and not the apk :upside_down_face:

Yeah, basically D8 defaulted to the non-multi-dex path in the second step, and just took classes.dex, hardcoded. I changed it now and D8 and multi-dex use the same code (gather all generated .dex files).

D8 changes are in and merged for tomorrow’s build (UseD8 still defaults to false, and the new option is AndroidMainDexListFile; both exposed in Project settings, too. I’d appreciate if you could put this thru it’s paces, and if all is well, I’ll make UseD8 default to true (if available, of course) next week.

1 Like

I’ll have to look into this more later, but my initial build with D8 is failing on this (and related) error:

d8> Interface `com.squareup.okhttp.Callback` not found. It's needed to make sure desugaring of `com.dropbox.core.http.OkHttpRequestor$AsyncCallback` is correct. Desugaring will assume that this interface has no default method.

A quick search led me to this Stack Overflow post which initially suggested simply disabling desugaring on D8, which can be done with the --no-desugaring switch. I’ll have to look into it a bit more though, since the docs say “Use this flag only if you don’t intend to compile Java bytecode that uses Java 8 language features.” This doesn’t seem like a safe assumption to make for an external library.

I often see Java 7/8 compat stuff in Android builds solved with Gradle’s setting, but I’m not sure when exactly to use it.

compileOptions {
     sourceCompatibility JavaVersion.VERSION_1_8
     targetCompatibility JavaVersion.VERSION_1_8
 }

EDIT:
This ultimately maps into javac.

I also happened to notice in the build log that AndroidPredex task is still always using DX instead of picking up on D8. My build ultimately fails on a “no dex files found” error. I assumed this was due to the preceding Java 8 compatibility warning, but perhaps it was due to AndroidPack making D8 assumptions that weren’t true.