Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 64 additions & 31 deletions cli/src/tasks/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { writeFileSync, readFileSync, existsSync } from 'fs-extra';
import { join } from 'path';
import rimraf from 'rimraf';
import { rimraf } from 'rimraf';
import { coerce, gte, lt } from 'semver';

import c from '../colors';
Expand Down Expand Up @@ -43,8 +43,8 @@ const plugins = [
'@capacitor/text-zoom',
'@capacitor/toast',
];
const coreVersion = '^8.0.0';
const pluginVersion = '^8.0.0';
const coreVersion = 'next';
const pluginVersion = 'next';
const gradleVersion = '9.5.1';
const iOSVersion = '16';
const kotlinVersion = '2.4.0';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we no longer be replacing kotlin version for classpath, and instead remove it? Because it's now bundled in AGP 9?

We added it in Capacitor 8 to fix an issue related to an app declaring the kotlin version in root build.gradle and it differing from the one Capacitor / Plugins use. Internal Slack Thread. But AGP bundles kotlin gradle plugin it's probably better to not have it in the app, and instead remove it altogether perhaps?

But also, we are using Kotlin 2.4.0 here and AGP 9.2 uses 2.3.10. But since it's a minor version change it should be ok (where last year it was a major version).

If you think this is better left to a separate task / PR let me know, but just wanted to highlight it.

Expand All @@ -56,14 +56,14 @@ export async function migrateCommand(config: Config, noprompt: boolean, packagem
}

const capMajor = await checkCapacitorMajorVersion(config);
if (capMajor < 7) {
fatal('Migrate can only be used on Capacitor 7, please use the CLI in Capacitor 7 to upgrade to 7 first');
if (capMajor < 8) {
fatal('Migrate can only be used on Capacitor 8, please use the CLI in Capacitor 8 to upgrade to 8 first');
}

const jdkMajor = await checkJDKMajorVersion();

if (jdkMajor < 21) {
logger.warn('Capacitor 8 requires JDK 21 or higher. Some steps may fail.');
logger.warn('Capacitor 9 requires JDK 21 or higher. Some steps may fail.');
}

const variablesAndClasspaths:
Expand All @@ -84,13 +84,13 @@ export async function migrateCommand(config: Config, noprompt: boolean, packagem
};

const monorepoWarning =
'Please note this tool is not intended for use in a mono-repo environment, you should migrate manually instead. Refer to https://capacitorjs.com/docs/next/updating/8-0';
'Please note this tool is not intended for use in a mono-repo environment, you should migrate manually instead. Refer to https://capacitorjs.com/docs/next/updating/9-0';

logger.info(monorepoWarning);

const { migrateconfirm } = noprompt
? { migrateconfirm: 'y' }
: await logPrompt(`Capacitor 8 sets a deployment target of iOS ${iOSVersion} and Android 17 (SDK 37). \n`, {
: await logPrompt(`Capacitor 9 sets a deployment target of iOS ${iOSVersion} and Android 17 (SDK 37). \n`, {
type: 'text',
name: 'migrateconfirm',
message: `Are you sure you want to migrate? (Y/n)`,
Expand Down Expand Up @@ -175,6 +175,10 @@ export async function migrateCommand(config: Config, noprompt: boolean, packagem
} else {
logger.warn('Skipped updating deployment target');
}

await runTask(`Migrating AppDelegate.swift`, () => {
return updateAppDelegate(join(config.ios.nativeTargetDirAbs, 'AppDelegate.swift'));
});
}

if (!installFailed) {
Expand Down Expand Up @@ -228,6 +232,23 @@ export async function migrateCommand(config: Config, noprompt: boolean, packagem
return updateAppBuildGradle(join(config.android.appDirAbs, 'build.gradle'));
});

// settings.gradle
await runTask(`Migrating settings.gradle file.`, () => {
return (async (): Promise<void> => {
const settingsPath = join(config.android.platformDirAbs, 'settings.gradle');
let txt = readFile(settingsPath);
if (!txt) {
return;
}
txt = txt.replace(`include ':capacitor-cordova-android-plugins'\n`, '');
txt = txt.replace(
`project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')\n`,
'',
);
writeFileSync(settingsPath, txt, { encoding: 'utf-8' });
})();
});

// Variables gradle
await runTask(`Migrating variables.gradle file.`, () => {
return (async (): Promise<void> => {
Expand Down Expand Up @@ -336,14 +357,14 @@ async function installLatestLibs(dependencyManager: string, runInstall: boolean,
for (const devDepKey of Object.keys(pkgJson['devDependencies'] || {})) {
if (libs.includes(devDepKey)) {
pkgJson['devDependencies'][devDepKey] = coreVersion;
} else if (plugins.includes(devDepKey)) {
} else if (plugins.includes(devDepKey) && !pkgJson['devDependencies'][devDepKey].startsWith('file:')) {
pkgJson['devDependencies'][devDepKey] = pluginVersion;
}
}
for (const depKey of Object.keys(pkgJson['dependencies'] || {})) {
if (libs.includes(depKey)) {
pkgJson['dependencies'][depKey] = coreVersion;
} else if (plugins.includes(depKey)) {
} else if (plugins.includes(depKey) && !pkgJson['dependencies'][depKey].startsWith('file:')) {
pkgJson['dependencies'][depKey] = pluginVersion;
}
}
Expand All @@ -366,18 +387,7 @@ async function installLatestLibs(dependencyManager: string, runInstall: boolean,
}

async function writeBreakingChanges() {
const breaking = [
'@capacitor/action-sheet',
'@capacitor/barcode-scanner',
'@capacitor/browser',
'@capacitor/camera',
'@capacitor/geolocation',
'@capacitor/google-maps',
'@capacitor/push-notifications',
'@capacitor/screen-orientation',
'@capacitor/splash-screen',
'@capacitor/status-bar',
];
const breaking = ['@capacitor/push-notifications', '@capacitor/splash-screen'];
const broken = [];
for (const lib of breaking) {
if (allDependencies[lib]) {
Expand All @@ -386,7 +396,7 @@ async function writeBreakingChanges() {
}
if (broken.length > 0) {
logger.info(
`IMPORTANT: Review https://capacitorjs.com/docs/next/updating/8-0#plugins for breaking changes in these plugins that you use: ${broken.join(
`IMPORTANT: Review https://capacitorjs.com/docs/next/updating/9-0#plugins for breaking changes in these plugins that you use: ${broken.join(
', ',
)}.`,
);
Expand Down Expand Up @@ -518,14 +528,19 @@ async function updateAppBuildGradle(filename: string) {
}
let replaced = txt;

const gradlePproperties = ['compileSdk', 'namespace', 'ignoreAssetsPattern'];
for (const prop of gradlePproperties) {
// Use updated Groovy DSL syntax with " = " assignment
const regex = new RegExp(`(^\\s*${prop})\\s+(?!=)(.+)$`, 'gm');
replaced = replaced.replace(regex, (_match, key, value) => {
return `${key} = ${value.trim()}`;
});
}
replaced = replaced.replace(`proguard-android.txt`, `proguard-android-optimize.txt`);
replaced = replaced.replace(' targetSdkVersion rootProject.ext.targetSdkVersion\n', '');

@OS-pedrogustavobilro OS-pedrogustavobilro Jul 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In AGP 9, users can change the behavior of targetSDK by setting android.sdk.defaultTargetSdkToCompileSdkIfUnset to false in gradle.properties - in which case it would default to the minSdk, which would break the app (don't know if it would compile, but you wouldn't be able to submit to play store). This will no longer be possible in AGP 10 (there's a warning in gradle related to this).

I don't know why anyone would do this voluntarily, but they could if they want to. What seems more likely to happen is users that migrated to AGP 9 while in Capacitor 8 - android.sdk.defaultTargetSdkToCompileSdkIfUnset=false; I tested Android Studio's AGP migration assistant and it adds that property.

So the way to overcome this issue would be one of or a combination of:

  1. Keep targetSdkVersion for now. Only remove it in a future version whenever we move to AGP 10. This would maybe require re-changing the template (although the template is new Capacitor apps, this point I'm making would be more for existing ones). For plugins it doesn't really matter if targetSdkVersion is there or not, because we don't run plugins on-device, only apps.
  2. Have the migrate command remove the properties added - Probably in a separate PR because I'm seeing there may be other properties that we may need to update for AGP 9. While I think we should do that, it has the nefarious effect of potentially removing flags that the particular Capacitor app needed, but there's no way for us to know if they were added automatically by Android Studio and have no actual impact, or if they have specific flags to fix specific issues in their native setup - will post more info on that internally in the next couple of days.

Let me know if this makes sense, and if you have a preference on the approach (only 1., only 2., both, other...)

replaced = replaced.replace(` implementation project(':capacitor-cordova-android-plugins')\n`, '');
replaced = replaced.replace(
`repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
}

`,
'',
);
writeFileSync(filename, replaced, 'utf-8');
}

Expand Down Expand Up @@ -622,3 +637,21 @@ async function updateAndroidManifest(filename: string) {
writeFileSync(filename, replaced, 'utf-8');
}
}

async function updateAppDelegate(filename: string) {
const txt = readFile(filename);
if (!txt) {
return;
}

if (txt.includes('@main')) {
return; // Probably already updated
}
// Since navigation was an optional change in Capacitor 7, attempting to add density and/or navigation

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment seems out of place? It sounds it's referring to changes in Android Manifest but this is AppDelegate.

const replaced = txt.replace('@UIApplicationMain', '@main');
if (!replaced.includes('@main')) {
logger.error(`Unable to replace @UIApplicationMain to @main in ${filename}. Try replacing it manually`);
} else {
writeFileSync(filename, replaced, 'utf-8');
}
}
Loading