blob: aa87123316b12dbd2f8155c6ec99dfb1fe891a49 [file] [log] [blame] [view]
# AGP Upgrade Assistant
The AGP Upgrade Assistant is a collection of infrastructure, user interfaces and
somewhat reusable components for the specific purpose of updating projects
consistently in response to a user request to upgrade the version of the Android
Gradle Plugin used in that project.
## Overview
The AGP Upgrade Assistant was introduced in Android Studio 4.2, extending or
substantially replacing existing infrastructure which special-cased three
actions to take:
1. Upgrading the version of AGP to the latest version;
2. Upgrading the version of Gradle to the version associated with that version
of AGP;
3. Conditionally adding the Google maven repository to the Gradle repositories,
if the AGP version was being upgraded from a version earlier than 3.0.0 to
one later than 3.0.0
The introduction of the AGP Upgrade Assistant in Android Studio 4.2 allowed
users to modify their build files if necessary to account for the switch of
default in AGP to use Java 8 (rather than the historic default of Java 7), as
well as to recommend modifying dependency configurations from the deprecated
`compile` to whichever of `api` and `implementation` was appropriate, and to
support migration of projects [from fabric to Firebase
Crashlytics](http://web.archive.org/web/20211017083605/https://firebase.google.com/docs/crashlytics/upgrade-sdk?platform=android).
Without the AGP Upgrade Assistant, each of these steps (or operations like them)
would have had to be done manually, prompted by a failure of the project to
sync, build, or function correctly once built or deployed.
Since its initial introduction, the AGP Upgrade Assistant has developed in
functionality, now offering features such as:
1. Upgrading the version of AGP to any version compatible with Android Studio
between the version currently in use and the latest version;
2. A user interface allowing both selection of changes, both coarse-grained (by
selectively enabling components to be part of the upgrade process) and
fine-grained (by enabling or disabling individual usages);
3. Unification of notions of compatibility with versions of the Android Gradle
plugin, as enforced by Gradle Sync;
4. Encoding of changes required for successful upgrades across two major
versions of AGP (7.0.0 and 8.0.0) as well as handling other compatibility
issues;
5. Handling of broadly-declarative build files, whether they are expressed in
Groovy, KotlinScript or some combination of those with Toml Version Catalogs.
## General Principles
### 100% success is unachievable {#100-success-is-unachievable}
For a variety of reasons, some of which are detailed below, the AGP Upgrade
Assistant can not guarantee to successfully update all Android projects, no
matter how limited the definition of success might be. For this reason, the
Upgrade Assistant's operation has to be as tolerant of failure, and as helpful
to the user in the face of failure, as possible.
### Non-declarative build configuration out of scope
A build as managed by Gradle is configured, in general, using code in languages
which are Turing complete. In order to make even partial success in updating
projects achievable, we make no attempt in the AGP Upgrade Assistant to handle
non-declarative build configuration beyond a few special cases such as appending
constant data to lists and maps. We do not at present attempt to handle
- substantial code in build files;
- buildSrc;
- a conventions plugin;
and uses of the AGP Upgrade Assistant in projects making substantial use of
these for build configuration will probably fail.
#### Detection of deprecated APIs
For code in build files and in `buildSrc`, we could in principle at least detect
the use of deprecated APIs, and block upgrades until those usages were removed.
(example: our attempt to do this with the removal of the Transform API in AGP
8.0, and similarly the old Variant API in future).
### User choice
We recognize that there is a significant constituency of users who would like to
believe that a tool such as the AGP Upgrade Assistant might Just Work, and we
intentionally do not disallow the user from trying other upgrade paths (such as
a single-step upgrade-to-latest) even if we are less than optimistic about the
chance that everything will operate correctly after such an upgrade. Thus, even
if we detect that a user is requesting an upgrade which we consider unlikely to
succeed, we allow the user to execute it anyway: it might get them closer.
However, if we can prove that the upgrade selected by the user cannot possibly
succeed, or that we do not have enough information to be able to make that
determination, we do have a mechanism for [blocking an
upgrade](#blocking-an-upgrade)
### Initial condition
The AGP Upgrade Assistant assumes that:
1. the state of the project when the Upgrade Assistant is launched is fully
reflected in the IDE (i.e. there are no changes to build files that have not
been imported to the IDE with a successful sync);
2. the state of the project when the Upgrade Assistant executes an upgrade is
not substantially different from the state when the Upgrade Assistant was
launched (or if it is, the user has performed explicit syncs and/or refreshes
using the UI provided).
### Success criterion
After an upgrade, we deem the upgrade successful if the project successfully
syncs (imports, loads into the IDE) without additional user intervention.
In particular, we make no analogous statement about the project successfully
building, deploying, or passing any accompanying tests; while we hope that
changes to allow the project to sync will tend to increase the chance that the
project will build and deploy successfully, we recognize that in the presence of
semantic changes in the Android Gradle Plugin, and version changes of Gradle
itself and other gradle plugins, can lead to differences in the build.
### Version number intervals
We impose a single “time” variable, based on the ordering of versions of the
Android Gradle plugin.
Many other relevant software artifacts also have versions that might have to be
examined or changed in order to effect an upgrade, such as Gradle itself, other
Gradle plugins, or environmental software such as the JDK used to run Gradle.
Changes or thresholds in those versions are mapped, directly or indirectly, to
the ordering defined by Android Gradle plugin versions; we make no attempt to
model the complete space of possible upgrade states and outcomes, instead
encoding required versions of those artifacts for any given Android Gradle
plugin version, and assuming that if the project is using a later version of
those artifacts pre-upgrade than the system thinks are required post-upgrade,
they continue to be valid. (One consequence of this is that we do not attempt
to lower the versions of any software used by a project.)
### Version suggestions
AGP's versioning scheme is partly-Semantic, in the sense that major changes in
behaviour are reserved for major version number increments, while patch-level
increments (after the .0 of any release series) are intended to include safe
bug-fix changes only.
This implies that some upgrades involve less risk than others. Upgrades to
newer patch-level versions should be almost risk-free; upgrades within a given
major series will involve some behavior changes, while changes to build files
required by upgrades between major series might be substantial, and there might
be subtle semantic changes that are not immediately obvious.
The Upgrade Assistant takes the opinionated view that under normal circumstances
we do not recommend upgrading through more than one major version number change
at once, and we only recommend upgrading the major version number from the last
version in the previous series. This is only a recommendation; the Upgrade
Assistant allows the user to select any published version that is not known to
be incompatible with the version of Android Studio it is running it.
#### Incremental or multi-step upgrades
The default version recommendation means that upgrading a project to the latest
version can take multiple steps. At present, the User Interface does not handle
multiple steps within one logical session; once one upgrade is complete, as far
as the Upgrade Assistant is concerned, it is in the past and not retrievable
through any more specialized means than Undo. It might be nice to revisit some
of the possibilities for a more integrated handling of multi-step upgrades.
## Architecture
### Implementation as Refactoring Processor
The Upgrade Assistant is implemented as a Refactoring Processor, albeit one with
a number of extra bells and whistles and substantial internal structure. We
have had to override some sections of the implementation of the
`BaseRefactoringProcessor` in order to provide some small amounts of custom
behaviour beyond that afforded through extensions.
#### Component refactoring processors
Some of the internal structure resides in the use of component refactoring
processors: each set of modifications to build configuration files for one
conceptual change should be contained within one component processor.
#### Independence of components
Historically, the AGP Version upgrade processor itself was somewhat privileged
in the architecture; however, this special status is now limited to the context
of presenting information to the user.
For a given project with a particular AGP version, each component computes its
necessity; for many processors dealing with some change in AGP behaviour, this
will involve comparing whether the AGP version is before, within or after a
region, bounded below by the version in which a new behaviour was introduced,
and above by the version in which the old behaviour was removed. These
processors can be said to have an "active region" in version number space.
All component processors are handled individually, and no component should
depend on another component being enabled or active; nor should they in general
depend on their usages being handled before or after those of a different
component, except: if the version number interval for one processor's active
region precedes another's, the processors can rely on the fact that all of the
earlier processor's actions will be taken before any of those of the later
processor.
Note: currently the above statement about ordering is only informally true, by
virtue of the order of the `componentRefactoringProcessors` defined in
`AgpUpgradeRefactoringProcessor`; it would be good to implement more of Allen's
(1983) logic and either enforce this by construction or validate it. It is
important in order to offer some guarantees about the composability of upgrades,
that the result of A→B and B→C be the same in textual form as A→C -- the
expectation is that any ordering of processors should give the same semantics,
but automated tests are easier to implement in terms of equality of outputs than
equality of Gradle Dsl semantics.
Note: this is shortly to be a problem that needs to be resolved. We already
have active processors which interact with properties in the `packagingOptions`
block of `android`:
- `AndroidManifestExtractNativeLibsToUseLegacyPackaging`
- `AndroidManifestUseEmbeddedDexToUseLegacyPackagingRefactoringProcessor`
- `MigratePackagingOptionsToJniLibsAndResourcesRefactoringProcessor`
But as of AGP 8.0.0-beta02, `packagingOptions` itself is deprecated in favour of
`packaging`, and we will also need to perform that migration. Making sure that
this happens without loss (and without writing nonsense) will not be completely
trivial.
#### Ordering of usages within components
It is sometimes important for the usages generated by a single component to be
processed in order; for example, to move Dsl properties to a different place in
the hierarchy, adding the property at the new place must be done while the value
of the old place is still available in the Dsl model. (Note that this could be
achieved by storing the value in the creation usage, or by using an atomic move
operation, but it can be convenient to use the ordering of usages within a
component).
Under normal operation, the usage order is preserved. However, executing the
proposed refactoring from the Preview (usage view) window returns the active
usage set to the AgpUpgradeRefactoringProcessor in arbitrary order, through some
internal (to the IntelliJ platform) use of an unordered set. We keep a list of
the usages previewed so as to be able to restore the original order of the
previewed usages.
#### Meta-components: move, rewrite, remove
A number of component upgrades involve fairly simple operations on the build
Dsl, corresponding to renaming or reparenting individual properties, deleting
them (either with some other mechanism for backwards-compatibility, or as a
final removal of something no longer supportable), or changing the preferred way
of referring to properties (for example, migrating from one overloaded operator
to separate operators for different-typed values). Component processors to do
these operations can be built from re-usable metacomponents, specified as fields
in the `PropertiesOperationsRefactoringInfo` data class; individual move,
rewrite or delete operations can also be used as part of the implementation of
general component processors.
#### Blocking an upgrade {#blocking-an-upgrade}
Individual components can *block* an upgrade. If an individual component
detects that the state of the project is such that it should be active, but that
it cannot successfully alter the project to lead to a successful upgrade, then
it should block, by returning a list of `BlockReason` objects from an override
of the `blockProcessorReasons()` member function.
If the processor is not mandatory for the upgrade, the user can disable it and
proceed with upgrading using any remaining processors. However, if the processor
is mandatory for the upgrade, the user is not offered the ability to override
the block through the Upgrade Assistant interface; they must make the necessary
modifications to remove the block *before* the Upgrade Assistant will allow its
managed upgrade process.
#### Re-use of component refactoring processors
In an ideal world, we would be able to re-use each component refactoring
processor directly to provide an Action to perform the changes to the build for
the corresponding behaviour change externally to the Upgrade Assistant, for
example as Refactoring menu items. This has not been explored fully as yet.
### Build file modification using gradle-dsl model
Most of the Component Refactoring Processors make changes to build files, and
most (all?) of them do so using the facilities of the gradle-dsl model. This
model provides a view of the build configuration, mostly independent of how that
configuration is expressed, and allows changes to be made to build and
configuration files whether they are expressed in Groovy, Kotlin or Gradle
Declarative Build Configuration Language.
The changes to the build model made by the processors accumulate within the
build model, with later queries to the model reflecting earlier changes, but are
only written out to file near the end of the Refactoring Process, in the
extending `performPsiSpoilingRefactoring()` method.
#### File modification outside the gradle-dsl model
Modifications to project files using other mechanisms (through VirtualFiles, or
even through standard Java IO) might be needed, if changes are needed to files
that are not covered by the gradle-dsl model. Any such changes require the
system to be notified of the changes for consistency. Individual component
processors can do this by adding the PsiFile corresponding to files changed in
this way to their `otherAffectedFiles` list; the IDE view of those files will be
updated at the end of the operation of the processor, performing any postponed
operations and committing documents as necessary.
### Modal interface for forced upgrade
When we detect a declared incompatibility between the project's declared version
of the Android Gradle Plugin and the versions of AGP supported by the running
Android Studio, we pop up a modal dialog informing the user of this, and
allowing them to run an upgrade with very limited customizeability. The limited
ability of the user to tweak the upgrade, coupled with the fact that we declare
ourselves incompatible between preview versions of AGP/Studio, despite often
being compatible and Sync at least partly working, suggests that we should
perhaps revisit this flow in the light of the better scope for compatibility we
have between AGP and Studio versions.
The modal interface is in some sense required for incompatible versions of AGP
and Studio, because if the versions are truly incompatible then we cannot trust
very much of the project information that we have: most of the operations of the
Upgrade Assistant do not depend on valid (Sync) models, though the precondition
really ought to be that they should be obtainable. On the other hand, the
incompatibility between different preview versions of AGP and Studio is at least
partly artificial, and could perhaps be relaxed if we implemented
producer/consumer model versioning.
#### Compatibility between AGP and Android Studio
More generally, there is the possibility in the future that AGP and Android
Studio release cycles will decouple at least somewhat. If they do, some thought
needs to go into how Android Studio should treat versions of AGP that are
released *after* themselves -- there is no realistic way of knowing what changes
a newer version of AGP might require.
### The tool window interface
In the more expected case of the AGP Upgrade Assistant being used when an
upgrade is merely recommended (or strongly recommended) rather than forced, we
provide a tool window interface rather than a modal one.
This tool window interface allows the user to customize several things about the
upgrade, with consistency maintained by various validators and UI triggers:
- the target AGP version: we do not enforce an upgrade to a particular version.
While the initial target is a recommended version (with somewhat complicated
rules for generating that recommendation; see [above](#version-suggestions)
for a prose description and `computeGradlePluginUpgradeState()` for the
details), the user can override this recommendation, taking larger or smaller
steps as they wish. The selection box enforces the well-formedness of the AGP
version string and the constraint that upgrades should go forwards in version
space only.
- for the project's current AGP version and the user's specified target version,
a number of upgrade components will be: preconditions (pre-upgrade), required
(upgrade) or recommended (applicable post-upgrade) steps. (Others will be
irrelevant to that combination of current and target version, and yet others
will in principle be applicable to that combination but would have no effect
on the particular project). For those upgrade components, the checkbox tree
maintains consistency: preconditions can only be unselected if the required
components are unselected, which itself can only happen if post-upgrade steps
are unselected. In the case of "upgrading" to the same version, where there
are no required steps, recommended steps are selected by default.
- if all components is unselected, the upgrade is blocked, or there is nothing
to do, the run and preview (see below) buttons are automatically disabled;
they are re-enabled if the user unblocks the upgrade or selects a component.
### Preview or Show Usages
Users would like to have a view of changes that will be made to their project,
usually expressing that as wanting a "diff view" or something. Preview, or Show
Usages, is not that, but it is nevertheless interesting. The Show Usages view
(available both from the tool window and -- more problematically, given that it
is not modal -- from the modal forced-upgrade dialog) represents the changes
that are to be made similarly to the usages in other refactoring previews
(e.g. a method or class rename), with some of the same affordances: including
the ability by users to remove individual usages from the action to be taken.
This allows fine-grained semantic customization of the operation of the
processor, but on a meaningful-change by meaningful-change basis, rather than
line-by-line: if a given usage's refactoring operation does multiple things,
they will all happen together or not at all.
### Reverting changes
We want to make it easy for users to revert changes made by the Upgrade
Assistant, because of our acknowledgment that [100% success is
unachievable](#100-success-is-unachievable).
#### Undo
The Upgrade Assistant integrates with Undo. The overall processor ensures that
Sync runs after the refactoring is complete; it also adds a
`BasicUndoableAction` to the UndoManager that ensures that Sync runs after Undo
(and Redo) operations.
The overall processor also manages any undo or redo hooks needed by component
processors, making sure to call them appropriately. At the moment the only
component processor making use of this is the one updating the project JDK to
meet compatibility requirements of AGP.
Note that it seems fairly easy to confuse Undo involving changes from a single
Action (such as a refactoring) in multiple files.
#### Local File History
We also provide a revert operation using the Local File History mechanism,
presented to the user as a button in the post-upgrade report. Using it might
not completely restore the state of the project, as for example changes to
e.g. the project JDK might not be captured in Local File History; we might need
to integrate better with the hooks implemented for Undo.
## Periodic maintenance
A few things related to the Upgrade Assistant need updating when we are close to
a release of Android Studio and/or AGP, mostly related to data and testing where
the AGP Upgrade Assistant is (close to) the source of historical truth.
### Required version of Gradle
AGP and Studio share a common view of the version of Gradle required for that
version of AGP, through `SdkConstants.GRADLE_LATEST_VERSION`. This means that
for a release of Studio, the Upgrade Assistant will automatically be aware of
the version of Gradle required for upgrades to the latest version of AGP.
However, the Upgrade Assistant also supports upgrades to versions of AGP that
are not the current, each of which might have its own required version of
Gradle; the Upgrade Assistant by design updates Gradle, if necessary, to the
minimal version required by the requested version of AGP. A table listing
compatibility requirements is maintained in `CompatibleGradleVersion`, for use
in the Upgrade Assistant but also in project templates and in tests; this table
should be essentially the same as the user-facing documentation at the [Android
Gradle Plugin release
notes](https://developer.android.com/studio/releases/gradle-plugin#updating-gradle).
### Required versions of other software
More infrequently, Android Studio and/or AGP change their distribution or
requirements regarding other external software: for example, the SDK build
tools, the NDK, or the JDK.
Optional or required upgrades to the JDK are handled by a dedicated component
refactoring processor, and changes to the table required versions need to be
made when a new version of the JDK is required by the system. At the moment
that table is local to the processor, in the `CompatibleJdkVersion` enum class.
At present, changes in required versions of the SDK build tools or the NDK are
not handled by the AGP Upgrade Assistant.
### Integration and end-to-end tests
The integration tests for the AGP Upgrade Assistant are, for infrastructural
reasons, run in shards each specialized to the *current* version of AGP for that
test. This is because it is relatively expensive to start a Gradle daemon
process, but once it has been started it can be shared between a number of tests
if each uses the same version of AGP. We verify that the upgrade succeeds (in
our terms, which implies syncing successfully), but rather than running sync on
the post-upgrade state of the project (which would require starting more Gradle
processes, slowing the tests down and using up our infra capacity) we verify
that the post-upgrade state as described by build-related files is equal to
golden files.
When a stable (or RC, or possibly beta) version of Studio is released, it should
be integrated into the `OldAgpTests` and `AgpVersionSoftwareEnvironment`
mechanisms in tests, and prebuilts made available, so that integration tests can
be run of the Upgrade Assistant's (on `studio-main`) ability to upgrade projects
to versions of AGP on the beta channel.
Tests of the upgrade process from canary to canary are currently done
partly-manually. Automating them would be good, but ideally we should not have
to upload prebuilds of every AGP canary. Perhaps we should upload the *first*
AGP alpha of every series, and verify that the Upgrade Assistant can upgrade to
the latest alpha / beta (but note that this might require a change in behaviour
of the Upgrade Assistant recommendations to accommodate the fact that the test
environment will not treat the "latest" version as having been published).
## Further reading
- James F. Allen, “[Maintaining Knowledge about Temporal
Intervals](http://cse.unl.edu/~choueiry/Documents/Allen-CACM1983.pdf)”.
Communications of the ACM 26:11, pp.832—843 (1983)
- (Google internal) [Index of AGP Upgrade Assistant
documents](https://docs.google.com/document/d/1wKkG15sj60UrLBbVLZD-w1BGryaQcin5juawMJ0GLpM/edit)