Change cache invalidation so that if any app extension's cache is invalid,
then invalidate the cache for the container app and all other app extensions.
This is the safest option when we're sharing code.
Don't use the global command line arguments to determine input, because that's
not the input we use for app extensions anymore.
Instead explicitly pass the input arguments when creating the cache.
Since neither mtouch nor mmmp is mkbundled anymore, the installed binary is in
fact a shell script.
This means that it's quite useless to check if the shell script has been
modified; instead check if the executing assembly has been modified (which
works now that we're not mkbundled anymore).
Allow creating temporary apps/extensions multiple times without re-creating temporary directories.
This makes it possible for tests to call CreateTemporaryApp|Extension multiple
times with different code, and have only the actual managed assembly change
between each time (which is useful for tests that verifies that cached builds
are used properly).
We must build each appex bundle before the container bundle, so that we can
compute the frameworks each appex the needs before bundling the container app.
Also there's no need to store the list of frameworks appex's need in a file,
since everything is now done in the same mtouch process.
Implement support for sharing both code and resources between app extensions
and their container app:
* AOT-compiled code. Each shared assembly is only AOT-compiled once, and if
the assembly is built to a framework or dynamic library, it will also only
be included once in the final app (as a framework or dynamic library in the
container app, referenced directly by the app extension). If the assemblies
are built to static objects there won't be any size improvements in the app,
but the build will be much faster, because the assemblies will only be AOT-
compiled once.
* Any resources related to managed assemblies (debug files, config files,
satellite assemblies) will be put in the container app only.
Since these improvements are significant, code sharing will be enabled by
default.
Test results
============
For an extreme test project with 7 extensions (embedded-frameworks)[1]:
with code sharing cycle 9 difference
build time 1m 47s 3m 33s -1m 46s = ~50% faster
app size 26 MB 131 MB -105 MB = ~80% smaller
For a more normal test project (MyTabbedApplication)[2] - this is a simple application with 1 extension:
with code sharing cycle 9 difference
build time 0m 44s 0m 48s -4s = ~ 8% faster
app size 23 MB 37 MB -15 MB = ~40% smaller
Another tvOS app with one extension also show similar gains (MyTVApp)[3]:
with code sharing cycle 9 difference
build time 0m 22s 0m 48s -26s = ~54% faster
app size 22 MB 62 MB -40 MB = ~65% smaller
[1]: https://github.com/rolfbjarne/embedded-frameworks
[2]: https://github.com/xamarin/xamarin-macios/tree/cycle9/msbuild/tests/MyTabbedApplication
[3]: https://github.com/xamarin/xamarin-macios/tree/cycle9/msbuild/tests/MyTVApp
Warn if mtouch loads an assembly from a different location than requested
(which might be because there are multiple assemblies with the same name).
Also rework the MT0023 check a bit by explicitly loading the root assembly
first, and then detecting if any loaded assemblies matches the root assembly.
This results in code that's a bit more obvious, and it also works correctly
with extensions (previously the entire MT0023 check was skipped for
extensions).
Allow the assembly build target name for frameworks to end with '.framework',
so that the following:
--assembly-build-target=@sdk=framework=Xamarin.Sdk.framework
doesn't end up creating Xamarin.Sdk.framework.framework.
If an extension is sharing code with the container app, then assemblies may be
placed in:
* The container app's Framework directory (for assemblies whose code is
shared, and the build action is 'framework'). We already handle this case.
* The container app's root directory (for assemblies whose code is shared, and
the build action is not 'framework'). This case we didn't handle, and we're
now fixing it.
* In the extension (for assemblies whose code is not shared). We already
handle this case.
Assemblies will not end up in the .monotouch-[32|64] subdirectory anymore
(unless they must because they're different), which means that it's not easy
to detect if an assembly really ends up in the subdirectory or not.
So modify tests to accept assemblies in either the root bundle directory, or
in the .monotouch-[32|64] subdirectory.
Assemblies can now be in frameworks.
Assemblies can also be in container apps (in their root directory, as well as
in frameworks in the container app).
So use the simplest brute-force method to find assemblies: look everywhere.
Store the location of every assembly that can't be deduced at runtime (i.e.
all assemblies that are build to frameworks, since there can be multiple
assemblies in each framework, and the framework name can be customized).
Detect when assemblies have native dependencies between them (which can happen
when there are multiple binding projects, and the native libraries in those
binding projects have dependencies between them), and add the proper link
arguments (this is only required when building to dynamic libraries or
frameworks, since otherwise everything is linked to one big binary and there
are no dependency problems).
https://bugzilla.xamarin.com/show_bug.cgi?id=43689
The previous build system kept a forward-pointing single linked list of tasks
to execute: task X had a list of subsequent tasks to execute. If task X was
up-to-date, it was not created (and the next tasks were directly added to the
list of tasks to execute).
In this world it became complicated to merge output from tasks (for instance
if the output of task X and task Y should be a consumed by a single task
producing a single output, since the corresponding task would end up in both
X's and Y's list of subsequent tasks).
Example: creating a single framework from the aot-compiled output of multiple
assemblies.
So I've reversed the logic: now we keep track of the final output, and then
each task has a list of dependencies that must be built.
This makes it trivial to create merging tasks (for the previous example, there
could for instance be a CreateFrameworkTask, where its dependencies would be
all the corresponding AotTasks).
We also always create every task, and then each task decides when its executed
whether it should do anything or not. This makes it unnecessary to 'forward-
delete' files when creating tasks (say you have three tasks, A, B, C; B
depends on A, and C depends on B; if A's output isn't up-to-date, it has to
delete its own output if it exists, otherwise B would not detect that it would
have to re-execute, because at task *creation* time, B's input hadn't
changed).
Additionally make it based on async/await, since much of the work happens in
externel processes (and we don't need to spin up additional threads just to
run external processes). This makes us have less code run on background
threads, which makes any issues with thread-safety less likely.