diff --git a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy index 4ebbf4e3b2..c95652727f 100644 --- a/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy +++ b/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy @@ -15,6 +15,7 @@ import com.flutter.gradle.BaseFlutterTask import com.flutter.gradle.Deeplink import com.flutter.gradle.DependencyVersionChecker import com.flutter.gradle.FlutterExtension +import com.flutter.gradle.FlutterPluginUtils import com.flutter.gradle.IntentFilterCheck import com.flutter.gradle.VersionUtils import groovy.xml.QName @@ -145,7 +146,7 @@ class FlutterPlugin implements Plugin { throw new GradleException("flutter.sdk must point to the Flutter SDK directory") } - engineVersion = useLocalEngine() + engineVersion = FlutterPluginUtils.shouldProjectUseLocalEngine(project) ? "+" // Match any version since there's only one. : "1.0.0-" + Paths.get(flutterRoot.absolutePath, "bin", "cache", "engine.stamp").toFile().text.trim() @@ -156,7 +157,7 @@ class FlutterPlugin implements Plugin { // Configure the Maven repository. String hostedRepository = System.getenv("FLUTTER_STORAGE_BASE_URL") ?: DEFAULT_MAVEN_HOST - String repository = useLocalEngine() + String repository = FlutterPluginUtils.shouldProjectUseLocalEngine(project) ? project.property(propLocalEngineRepo) : "$hostedRepository/${engineRealm}download.flutter.io" rootProject.allprojects { @@ -188,7 +189,7 @@ class FlutterPlugin implements Plugin { // By default, assembling APKs generates fat APKs if multiple platforms are passed. // Configuring split per ABI allows to generate separate APKs for each abi. // This is a noop when building a bundle. - if (shouldSplitPerAbi()) { + if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) { project.android { splits { abi { @@ -213,7 +214,7 @@ class FlutterPlugin implements Plugin { getTargetPlatforms().each { targetArch -> String abiValue = PLATFORM_ARCH_MAP[targetArch] project.android { - if (shouldSplitPerAbi()) { + if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) { splits { abi { include(abiValue) @@ -269,14 +270,14 @@ class FlutterPlugin implements Plugin { // This limitation has been removed experimentally in gradle plugin version 4.2, so we can remove // this check when we upgrade to 4.2+ gradle. Currently, deferred components apps may see // increased app size due to this. - if (shouldShrinkResources(project)) { + if (FlutterPluginUtils.shouldShrinkResources(project)) { release { // Enables code shrinking, obfuscation, and optimization for only // your project's release build type. minifyEnabled(true) // Enables resource shrinking, which is performed by the Android Gradle plugin. // The resource shrinker can't be used for libraries. - shrinkResources(isBuiltAsApp(project)) + shrinkResources(FlutterPluginUtils.isBuiltAsApp(project)) // Fallback to `android/app/proguard-rules.pro`. // This way, custom Proguard rules can be configured as needed. proguardFiles(project.android.getDefaultProguardFile("proguard-android-optimize.txt"), flutterProguardRules, "proguard-rules.pro") @@ -284,7 +285,7 @@ class FlutterPlugin implements Plugin { } } - if (useLocalEngine()) { + if (FlutterPluginUtils.shouldProjectUseLocalEngine(project)) { // This is required to pass the local engine to flutter build aot. String engineOutPath = project.property("local-engine-out") File engineOut = project.file(engineOutPath) @@ -304,21 +305,6 @@ class FlutterPlugin implements Plugin { project.android.buildTypes.all(this.&addFlutterDependencies) } - private static Boolean shouldShrinkResources(Project project) { - final String propShrink = "shrink" - if (project.hasProperty(propShrink)) { - return project.property(propShrink).toBoolean() - } - return true - } - - private static String toCamelCase(List parts) { - if (parts.empty) { - return "" - } - return "${parts[0]}${parts[1..-1].collect { it.capitalize() }.join('')}" - } - private static Properties readPropertiesIfExist(File propertiesFile) { Properties result = new Properties() if (propertiesFile.exists()) { @@ -327,24 +313,6 @@ class FlutterPlugin implements Plugin { return result } - private static Boolean isBuiltAsApp(Project project) { - // Projects are built as applications when the they use the `com.android.application` - // plugin. - return project.plugins.hasPlugin("com.android.application") - } - - private static void addApiDependencies(Project project, String variantName, Object dependency, Closure config = null) { - String configuration - // `compile` dependencies are now `api` dependencies. - try{ - project.getConfigurations().named("api") - configuration = "${variantName}Api" - } catch(UnknownTaskException ignored) { - configuration = "${variantName}Compile" - } - project.dependencies.add(configuration, dependency, config) - } - // Add a task that can be called on flutter projects that prints the Java version used in Gradle. // // Format of the output of this task can be used in debugging what version of Java Gradle is using. @@ -502,20 +470,6 @@ class FlutterPlugin implements Plugin { } } - /** - * Returns a Flutter build mode suitable for the specified Android buildType. - * - * @return "debug", "profile", or "release" (fall-back). - */ - private static String buildModeFor(BuildType buildType) { - if (buildType.name == "profile") { - return "profile" - } else if (buildType.debuggable) { - return "debug" - } - return "release" - } - /** * Adds the dependencies required by the Flutter project. * This includes: @@ -523,8 +477,8 @@ class FlutterPlugin implements Plugin { * 2. libflutter.so */ void addFlutterDependencies(BuildType buildType) { - String flutterBuildMode = buildModeFor(buildType) - if (!supportsBuildMode(flutterBuildMode)) { + String flutterBuildMode = FlutterPluginUtils.buildModeFor(buildType) + if (!FlutterPluginUtils.supportsBuildMode(project, flutterBuildMode)) { return } // The embedding is set as an API dependency in a Flutter plugin. @@ -536,14 +490,14 @@ class FlutterPlugin implements Plugin { // embedding. List> pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency = flutterBuildMode == "release" ? getPluginListWithoutDevDependencies(project) : getPluginList(project); if (!isFlutterAppProject() || pluginsThatIncludeFlutterEmbeddingAsTransitiveDependency.size() == 0) { - addApiDependencies(project, buildType.name, + FlutterPluginUtils.addApiDependencies(project, buildType.name, "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion") } List platforms = getTargetPlatforms().collect() platforms.each { platform -> String arch = PLATFORM_ARCH_MAP[platform].replace("-", "_") // Add the `libflutter.so` dependency. - addApiDependencies(project, buildType.name, + FlutterPluginUtils.addApiDependencies(project, buildType.name, "io.flutter:${arch}_$flutterBuildMode:$engineVersion") } } @@ -586,14 +540,15 @@ class FlutterPlugin implements Plugin { try { // Read the contents of the settings.gradle file. // Remove block/line comments - String settingsText = settingsGradleFile(project).text + String settingsText = FlutterPluginUtils.getSettingsGradleFileFromProjectDir(project.projectDir, project.logger).text settingsText = settingsText.replaceAll(/(?s)\/\*.*?\*\//, '').replaceAll(/(?m)\/\/.*$/, '') if (!settingsText.contains("'.flutter-plugins'")) { return } } catch (FileNotFoundException ignored) { - throw new GradleException("settings.gradle/settings.gradle.kts does not exist: ${settingsGradleFile(project).absolutePath}") + throw new GradleException("settings.gradle/settings.gradle.kts does not exist: " + + "${FlutterPluginUtils.getSettingsGradleFileFromProjectDir(project.projectDir, project.logger).absolutePath}") } // TODO(matanlurey): https://github.com/flutter/flutter/issues/48918. project.logger.quiet("Warning: This project is still reading the deprecated '.flutter-plugins. file.") @@ -607,7 +562,7 @@ class FlutterPlugin implements Plugin { if (pluginProject == null) { // Plugin was not included in `settings.gradle`, but is listed in `.flutter-plugins`. project.logger.error("Plugin project :${it.name} listed, but not found. Please fix your settings.gradle/settings.gradle.kts.") - } else if (pluginSupportsAndroidPlatform(pluginProject)) { + } else if (FlutterPluginUtils.pluginSupportsAndroidPlatform(pluginProject)) { // Plugin has a functioning `android` folder and is included successfully, although it's not supported. // It must be configured nonetheless, to not throw an "Unresolved reference" exception. configurePluginProject(it) @@ -618,53 +573,6 @@ class FlutterPlugin implements Plugin { } } - // TODO(54566): Can remove this function and its call sites once resolved. - /** - * Returns `true` if the given project is a plugin project having an `android` directory - * containing a `build.gradle` or `build.gradle.kts` file. - */ - private static Boolean pluginSupportsAndroidPlatform(Project project) { - File buildGradle = new File(project.projectDir.parentFile, "android" + File.separator + "build.gradle") - File buildGradleKts = new File(project.projectDir.parentFile, "android" + File.separator + "build.gradle.kts") - return buildGradle.exists() || buildGradleKts.exists() - } - - /** - * Returns the Gradle build script for the build. When both Groovy and - * Kotlin variants exist, then Groovy (build.gradle) is preferred over - * Kotlin (build.gradle.kts). This is the same behavior as Gradle 8.5. - */ - private static File buildGradleFile(Project project) { - File buildGradle = new File(project.projectDir.parentFile, "app" + File.separator + "build.gradle") - File buildGradleKts = new File(project.projectDir.parentFile, "app" + File.separator + "build.gradle.kts") - if (buildGradle.exists() && buildGradleKts.exists()) { - project.logger.error( - "Both build.gradle and build.gradle.kts exist, so " + - "build.gradle.kts is ignored. This is likely a mistake." - ) - } - - return buildGradle.exists() ? buildGradle : buildGradleKts - } - - /** - * Returns the Gradle settings script for the build. When both Groovy and - * Kotlin variants exist, then Groovy (settings.gradle) is preferred over - * Kotlin (settings.gradle.kts). This is the same behavior as Gradle 8.5. - */ - private static File settingsGradleFile(Project project) { - File settingsGradle = new File(project.projectDir.parentFile, "settings.gradle") - File settingsGradleKts = new File(project.projectDir.parentFile, "settings.gradle.kts") - if (settingsGradle.exists() && settingsGradleKts.exists()) { - project.logger.error( - "Both settings.gradle and settings.gradle.kts exist, so " + - "settings.gradle.kts is ignored. This is likely a mistake." - ) - } - - return settingsGradle.exists() ? settingsGradle : settingsGradleKts - } - /** Adds the plugin project dependency to the app project. */ private void configurePluginProject(Map pluginObject) { assert(pluginObject.name instanceof String) @@ -687,11 +595,11 @@ class FlutterPlugin implements Plugin { } Closure addEmbeddingDependencyToPlugin = { BuildType buildType -> - String flutterBuildMode = buildModeFor(buildType) + String flutterBuildMode = FlutterPluginUtils.buildModeFor(buildType) // In AGP 3.5, the embedding must be added as an API implementation, // so java8 features are desugared against the runtime classpath. // For more, see https://github.com/flutter/flutter/issues/40126 - if (!supportsBuildMode(flutterBuildMode)) { + if (!FlutterPluginUtils.supportsBuildMode(project, flutterBuildMode)) { return } if (!pluginProject.hasProperty("android")) { @@ -707,7 +615,7 @@ class FlutterPlugin implements Plugin { // // See https://issuetracker.google.com/139821726, and // https://github.com/flutter/flutter/issues/72185 for more details. - addApiDependencies( + FlutterPluginUtils.addApiDependencies( pluginProject, buildType.name, "io.flutter:flutter_embedding_$flutterBuildMode:$engineVersion" @@ -805,9 +713,10 @@ class FlutterPlugin implements Plugin { for (Tuple2 pluginToCompileSdkVersion : pluginsWithHigherSdkVersion) { project.logger.error("- ${pluginToCompileSdkVersion.v1} compiles against Android SDK ${pluginToCompileSdkVersion.v2}") } + File buildGradleFile = FlutterPluginUtils.getBuildGradleFileFromProjectDir(project.projectDir, project.logger) project.logger.error("""\ Fix this issue by compiling against the highest Android SDK version (they are backward compatible). - Add the following to ${buildGradleFile(project).path}: + Add the following to ${buildGradleFile.path}: android { compileSdk = ${maxPluginCompileSdkVersion} @@ -820,9 +729,10 @@ class FlutterPlugin implements Plugin { for (Tuple2 pluginToNdkVersion : pluginsWithDifferentNdkVersion) { project.logger.error("- ${pluginToNdkVersion.v1} requires Android NDK ${pluginToNdkVersion.v2}") } + File buildGradleFile = FlutterPluginUtils.getBuildGradleFileFromProjectDir(project.projectDir, project.logger) project.logger.error("""\ Fix this issue by using the highest Android NDK version (they are backward compatible). - Add the following to ${buildGradleFile(project).path}: + Add the following to ${buildGradleFile.path}: android { ndkVersion = \"${maxPluginNdkVersion}\" @@ -857,7 +767,7 @@ class FlutterPlugin implements Plugin { } project.android.buildTypes.each { buildType -> - String flutterBuildMode = buildModeFor(buildType) + String flutterBuildMode = FlutterPluginUtils.buildModeFor(buildType) if (flutterBuildMode == "release" && pluginObject.dev_dependency) { // This plugin is a dev dependency will not be included in the // release build, so no need to add its dependencies. @@ -892,7 +802,7 @@ class FlutterPlugin implements Plugin { */ private List> getPluginList(Project project) { if (pluginList == null) { - pluginList = project.ext.nativePluginLoader.getPlugins(getFlutterSourceDirectory()) + pluginList = project.ext.nativePluginLoader.getPlugins(FlutterPluginUtils.getFlutterSourceDirectory(project)) } return pluginList } @@ -920,7 +830,7 @@ class FlutterPlugin implements Plugin { /** Gets the plugins dependencies from `.flutter-plugins-dependencies`. */ private List> getPluginDependencies(Project project) { if (pluginDependencies == null) { - Map meta = project.ext.nativePluginLoader.getDependenciesMetadata(getFlutterSourceDirectory()) + Map meta = project.ext.nativePluginLoader.getDependenciesMetadata(FlutterPluginUtils.getFlutterSourceDirectory(project)) if (meta == null) { pluginDependencies = [] } else { @@ -951,115 +861,6 @@ class FlutterPlugin implements Plugin { } } - private Boolean shouldSplitPerAbi() { - return project.findProperty("split-per-abi")?.toBoolean() ?: false - } - - private Boolean useLocalEngine() { - return project.hasProperty(propLocalEngineRepo) - } - - private Boolean isVerbose() { - return project.findProperty("verbose")?.toBoolean() ?: false - } - - /** Whether to build the debug app in "fast-start" mode. */ - private Boolean isFastStart() { - return project.findProperty("fast-start")?.toBoolean() ?: false - } - - /** - * Returns true if the build mode is supported by the current call to Gradle. - * This only relevant when using a local engine. Because the engine - * is built for a specific mode, the call to Gradle must match that mode. - */ - private Boolean supportsBuildMode(String flutterBuildMode) { - if (!useLocalEngine()) { - return true - } - final String propLocalEngineBuildMode = "local-engine-build-mode" - assert(project.hasProperty(propLocalEngineBuildMode)) - // Don't configure dependencies for a build mode that the local engine - // doesn't support. - return project.property(propLocalEngineBuildMode) == flutterBuildMode - } - - /** - * Gets the directory that contains the Flutter source code. - * This is the directory containing the `android/` directory. - */ - private File getFlutterSourceDirectory() { - if (project.flutter.source == null) { - throw new GradleException("Must provide Flutter source directory") - } - return project.file(project.flutter.source) - } - - /** - * Gets the target file. This is typically `lib/main.dart`. - */ - private String getFlutterTarget() { - String target = project.flutter.target ?: "lib/main.dart" - final String propTarget = "target" - if (project.hasProperty(propTarget)) { - target = project.property(propTarget) - } - return target - } - - // TODO: Remove this AGP hack. https://github.com/flutter/flutter/issues/109560 - /** - * In AGP 4.0, the Android linter task depends on the JAR tasks that generate `libapp.so`. - * When building APKs, this causes an issue where building release requires the debug JAR, - * but Gradle won't build debug. - * - * To workaround this issue, only configure the JAR task that is required given the task - * from the command line. - * - * The AGP team said that this issue is fixed in Gradle 7.0, which isn't released at the - * time of adding this code. Once released, this can be removed. However, after updating to - * AGP/Gradle 7.2.0/7.5, removing this hack still causes build failures. Further - * investigation necessary to remove this. - * - * Tested cases: - * * `./gradlew assembleRelease` - * * `./gradlew app:assembleRelease.` - * * `./gradlew assemble{flavorName}Release` - * * `./gradlew app:assemble{flavorName}Release` - * * `./gradlew assemble.` - * * `./gradlew app:assemble.` - * * `./gradlew bundle.` - * * `./gradlew bundleRelease.` - * * `./gradlew app:bundleRelease.` - * - * Related issues: - * https://issuetracker.google.com/issues/158060799 - * https://issuetracker.google.com/issues/158753935 - */ - private boolean shouldConfigureFlutterTask(Task assembleTask) { - List cliTasksNames = project.gradle.startParameter.taskNames - if (cliTasksNames.size() != 1 || !cliTasksNames.first().contains("assemble")) { - return true - } - String taskName = cliTasksNames.first().split(":").last() - if (taskName == "assemble") { - return true - } - if (taskName == assembleTask.name) { - return true - } - if (taskName.endsWith("Release") && assembleTask.name.endsWith("Release")) { - return true - } - if (taskName.endsWith("Debug") && assembleTask.name.endsWith("Debug")) { - return true - } - if (taskName.endsWith("Profile") && assembleTask.name.endsWith("Profile")) { - return true - } - return false - } - private boolean isFlutterAppProject() { return project.android.hasProperty("applicationVariants") } @@ -1145,7 +946,7 @@ class FlutterPlugin implements Plugin { } List targetPlatforms = getTargetPlatforms() def addFlutterDeps = { variant -> - if (shouldSplitPerAbi()) { + if (FlutterPluginUtils.shouldProjectSplitPerAbi(project)) { variant.outputs.each { output -> // Assigns the new version code to versionCodeOverride, which changes the version code // for only the output APK, not for the variant itself. Skipping this step simply @@ -1176,9 +977,9 @@ class FlutterPlugin implements Plugin { } boolean isUsedAsSubproject = packageAssets && cleanPackageAssets && !isBuildingAar - String variantBuildMode = buildModeFor(variant.buildType) + String variantBuildMode = FlutterPluginUtils.buildModeFor(variant.buildType) String flavorValue = variant.getFlavorName() - String taskName = toCamelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name]) + String taskName = FlutterPluginUtils.toCamelCase(["compile", FLUTTER_BUILD_PREFIX, variant.name]) // Be careful when configuring task below, Groovy has bizarre // scoping rules: writing `verbose isVerbose()` means calling // `isVerbose` on the task itself - which would return `verbose` @@ -1193,14 +994,14 @@ class FlutterPlugin implements Plugin { localEngine(this.localEngine) localEngineHost(this.localEngineHost) localEngineSrcPath(this.localEngineSrcPath) - targetPath(getFlutterTarget()) - verbose(this.isVerbose()) - fastStart(this.isFastStart()) + targetPath(FlutterPluginUtils.getFlutterTarget(project)) + verbose(FlutterPluginUtils.isProjectVerbose(project)) + fastStart(FlutterPluginUtils.isProjectFastStart(project)) fileSystemRoots(fileSystemRootsValue) fileSystemScheme(fileSystemSchemeValue) trackWidgetCreation(trackWidgetCreationValue) targetPlatformValues = targetPlatforms - sourceDir(getFlutterSourceDirectory()) + sourceDir(FlutterPluginUtils.getFlutterSourceDirectory(project)) intermediateDir(project.file(project.layout.buildDirectory.dir("$INTERMEDIATES_DIR/flutter/${variant.name}/"))) frontendServerStarterPath(frontendServerStarterPathValue) extraFrontEndOptions(extraFrontEndOptionsValue) @@ -1232,7 +1033,7 @@ class FlutterPlugin implements Plugin { } // Copy the native assets created by build.dart and placed in build/native_assets by flutter assemble. // The `$project.layout.buildDirectory` is '.android/Flutter/build/' instead of 'build/'. - String buildDir = "${getFlutterSourceDirectory()}/build" + String buildDir = "${FlutterPluginUtils.getFlutterSourceDirectory(project)}/build" String nativeAssetsDir = "${buildDir}/native_assets/android/jniLibs/lib" from("${nativeAssetsDir}/${abi}") { include("*.so") @@ -1243,7 +1044,7 @@ class FlutterPlugin implements Plugin { } } Task packJniLibsTask = packJniLibsTaskProvider.get() - addApiDependencies(project, variant.name, project.files { + FlutterPluginUtils.addApiDependencies(project, variant.name, project.files { packJniLibsTask }) TaskProvider copyFlutterAssetsTaskProvider = project.tasks.register( @@ -1255,7 +1056,7 @@ class FlutterPlugin implements Plugin { // See https://docs.gradle.org/current/javadoc/org/gradle/api/file/ConfigurableFilePermissions.html // See https://github.com/flutter/flutter/pull/50047 - if (compareVersionStrings(currentGradleVersion, "8.3") >= 0) { + if (FlutterPluginUtils.compareVersionStrings(currentGradleVersion, "8.3") >= 0) { filePermissions { user { read = true @@ -1310,7 +1111,7 @@ class FlutterPlugin implements Plugin { AbstractAppExtension android = (AbstractAppExtension) project.extensions.findByName("android") android.applicationVariants.configureEach { variant -> Task assembleTask = variant.assembleProvider.get() - if (!shouldConfigureFlutterTask(assembleTask)) { + if (!FlutterPluginUtils.shouldConfigureFlutterTask(project, assembleTask)) { return } Task copyFlutterAssetsTask = addFlutterDeps(variant) @@ -1340,7 +1141,7 @@ class FlutterPlugin implements Plugin { if (variant.flavorName != null && !variant.flavorName.isEmpty()) { filename += "-${variant.flavorName.toLowerCase()}" } - filename += "-${buildModeFor(variant.buildType)}" + filename += "-${FlutterPluginUtils.buildModeFor(variant.buildType)}" project.copy { from new File("$outputDirectoryStr/${output.outputFileName}") into new File("${project.layout.buildDirectory.dir("outputs/flutter-apk").get()}") @@ -1372,7 +1173,7 @@ class FlutterPlugin implements Plugin { Task copyFlutterAssetsTask appProject.android.applicationVariants.all { appProjectVariant -> Task appAssembleTask = appProjectVariant.assembleProvider.get() - if (!shouldConfigureFlutterTask(appAssembleTask)) { + if (!FlutterPluginUtils.shouldConfigureFlutterTask(project, appAssembleTask)) { return } // Find a compatible application variant in the host app. @@ -1394,8 +1195,8 @@ class FlutterPlugin implements Plugin { // (e.g. `buildType.debuggable = true`), then the equivalent Flutter // variant is `debug`. // 3. Otherwise, the equivalent Flutter variant is `release`. - String variantBuildMode = buildModeFor(libraryVariant.buildType) - if (buildModeFor(appProjectVariant.buildType) != variantBuildMode) { + String variantBuildMode = FlutterPluginUtils.buildModeFor(libraryVariant.buildType) + if (FlutterPluginUtils.buildModeFor(appProjectVariant.buildType) != variantBuildMode) { return } copyFlutterAssetsTask = copyFlutterAssetsTask ?: addFlutterDeps(libraryVariant) @@ -1410,50 +1211,6 @@ class FlutterPlugin implements Plugin { configurePlugins(project) detectLowCompileSdkVersionOrNdkVersion() } - - // compareTo implementation of version strings in the format of ints and periods - // Requires non null objects. - // Will not crash on RC candidate strings but considers all RC candidates the same version. - static int compareVersionStrings(String firstString, String secondString) { - List firstVersion = firstString.tokenize(".") - List secondVersion = secondString.tokenize(".") - - int commonIndices = Math.min(firstVersion.size(), secondVersion.size()) - - for (int i = 0; i < commonIndices; i++) { - String firstAtIndex = firstVersion[i] - String secondAtIndex = secondVersion[i] - int firstInt = 0 - int secondInt = 0 - try { - if (firstAtIndex.contains("-")) { - // Strip any chars after "-". For example "8.6-rc-2" - firstAtIndex = firstAtIndex.substring(0, firstAtIndex.indexOf('-')) - } - firstInt = firstAtIndex.toInteger() - } catch (NumberFormatException nfe) { - println(nfe) - } - try { - if (firstAtIndex.contains("-")) { - // Strip any chars after "-". For example "8.6-rc-2" - secondAtIndex = secondAtIndex.substring(0, secondAtIndex.indexOf('-')) - } - secondInt = secondAtIndex.toInteger() - } catch (NumberFormatException nfe) { - println(nfe) - } - - if (firstInt != secondInt) { - // <=> in groovy delegates to compareTo - return firstInt <=> secondInt - } - } - - // If we got this far then all the common indices are identical, so whichever version is longer must be more recent - return firstVersion.size() <=> secondVersion.size() - } - } class FlutterTask extends BaseFlutterTask { @@ -1525,5 +1282,4 @@ class FlutterTask extends BaseFlutterTask { void build() { buildBundle() } - } diff --git a/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt b/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt index d1823125b7..79821f1d74 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/DependencyVersionChecker.kt @@ -164,9 +164,9 @@ object DependencyVersionChecker { } } - private fun generateAssembleTaskName(it: Variant) = "$ASSEMBLE_PREFIX${it.name.capitalize()}" + private fun generateAssembleTaskName(it: Variant) = "$ASSEMBLE_PREFIX${FlutterPluginUtils.capitalize(it.name)}" - private fun generateMinSdkCheckTaskName(it: Variant) = "${it.name.capitalize()}$MIN_SDK_CHECK_TASK_POSTFIX" + private fun generateMinSdkCheckTaskName(it: Variant) = "${FlutterPluginUtils.capitalize(it.name)}$MIN_SDK_CHECK_TASK_POSTFIX" private fun getMinSdkVersion( project: Project, diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt index b84f5e6283..ab41e607d1 100644 --- a/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterExtension.kt @@ -39,7 +39,7 @@ open class FlutterExtension { * Specifies the relative directory to the Flutter project directory. * In an app project, this is ../.. since the app's Gradle build file is under android/app. */ - var source: String = "../.." + var source: String? = "../.." /** Allows to override the target file. Otherwise, the target is lib/main.dart. */ var target: String? = null diff --git a/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt new file mode 100644 index 0000000000..2f4133c5a0 --- /dev/null +++ b/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt @@ -0,0 +1,370 @@ +package com.flutter.gradle + +import com.android.builder.model.BuildType +import groovy.lang.Closure +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.UnknownTaskException +import org.gradle.api.logging.Logger +import java.io.File + +/** + * A collection of static utility functions used by the Flutter Gradle Plugin. + */ +object FlutterPluginUtils { + // Gradle properties. + internal const val PROP_SHOULD_SHRINK_RESOURCES = "shrink" + internal const val PROP_SPLIT_PER_ABI = "split-per-abi" + internal const val PROP_LOCAL_ENGINE_REPO = "localEngineRepo" + internal const val PROP_IS_VERBOSE = "verbose" + internal const val PROP_IS_FAST_START = "fast-start" + internal const val PROP_TARGET = "target" + internal const val PROP_LOCAL_ENGINE_BUILD_MODE = "local-engine-build-mode" + + // ----------------- Methods for string manipulation and comparison. ----------------- + + @JvmStatic + fun toCamelCase(parts: List): String { + if (parts.isEmpty()) { + return "" + } + return parts[0] + + parts.drop(1).joinToString("") { capitalize(it) } + } + + // Kotlin's capitalize function is deprecated, but the suggested replacement uses syntax that + // our minimum version doesn't support yet. Centralize the use to one place, so that when our + // minimum version does support the replacement we can replace by changing a single line. + @JvmStatic + @Suppress("DEPRECATION") + internal fun capitalize(string: String): String = string.capitalize() + + // compareTo implementation of version strings in the format of ints and periods + // Will not crash on RC candidate strings but considers all RC candidates the same version. + // Returns -1 if firstString < secondString, 0 if firstString == secondString, 1 if firstString > secondString + @JvmStatic + @JvmName("compareVersionStrings") + internal fun compareVersionStrings( + firstString: String, + secondString: String + ): Int { + val firstVersion = firstString.split(".") + val secondVersion = secondString.split(".") + + val commonIndices = minOf(firstVersion.size, secondVersion.size) + + for (i in 0 until commonIndices) { + var firstAtIndex = firstVersion[i] + var secondAtIndex = secondVersion[i] + var firstInt = 0 + var secondInt = 0 + + // Strip any chars after "-". For example "8.6-rc-2" + firstAtIndex = firstAtIndex.substringBefore("-") + try { + firstInt = firstAtIndex.toInt() + } catch (nfe: NumberFormatException) { + println(nfe) + } + + secondAtIndex = secondAtIndex.substringBefore("-") + try { + secondInt = secondAtIndex.toInt() + } catch (nfe: NumberFormatException) { + println(nfe) + } + + val comparisonResult = firstInt.compareTo(secondInt) + if (comparisonResult != 0) { + return comparisonResult + } + } + + // If we got this far then all the common indices are identical, so whichever version is longer must be more recent + return firstVersion.size.compareTo(secondVersion.size) + } + + // ----------------- Methods that interact primarily with the Gradle project. ----------------- + + @JvmStatic + @JvmName("shouldShrinkResources") + fun shouldShrinkResources(project: Project): Boolean { + if (project.hasProperty(PROP_SHOULD_SHRINK_RESOURCES)) { + val propertyValue = project.property(PROP_SHOULD_SHRINK_RESOURCES) + return propertyValue.toString().toBoolean() + } + return true + } + + // TODO(54566): Can remove this function and its call sites once resolved. + + /** + * Returns `true` if the given project is a plugin project having an `android` directory + * containing a `build.gradle` or `build.gradle.kts` file. + */ + @JvmStatic + @JvmName("pluginSupportsAndroidPlatform") + internal fun pluginSupportsAndroidPlatform(project: Project): Boolean { + val buildGradle = File(File(project.projectDir.parentFile, "android"), "build.gradle") + val buildGradleKts = + File(File(project.projectDir.parentFile, "android"), "build.gradle.kts") + return buildGradle.exists() || buildGradleKts.exists() + } + + /** + * Returns the Gradle settings script for the build. When both Groovy and + * Kotlin variants exist, then Groovy (settings.gradle) is preferred over + * Kotlin (settings.gradle.kts). This is the same behavior as Gradle 8.5. + */ + @JvmStatic + @JvmName("getSettingsGradleFileFromProjectDir") + internal fun getSettingsGradleFileFromProjectDir( + projectDirectory: File, + logger: Logger + ): File { + val settingsGradle = File(projectDirectory.parentFile, "settings.gradle") + val settingsGradleKts = File(projectDirectory.parentFile, "settings.gradle.kts") + if (settingsGradle.exists() && settingsGradleKts.exists()) { + logger.error( + """ + Both settings.gradle and settings.gradle.kts exist, so + settings.gradle.kts is ignored. This is likely a mistake. + """.trimIndent() + ) + } + + return if (settingsGradle.exists()) settingsGradle else settingsGradleKts + } + + /** + * Returns the Gradle build script for the build. When both Groovy and + * Kotlin variants exist, then Groovy (build.gradle) is preferred over + * Kotlin (build.gradle.kts). This is the same behavior as Gradle 8.5. + */ + @JvmStatic + @JvmName("getBuildGradleFileFromProjectDir") + internal fun getBuildGradleFileFromProjectDir( + projectDirectory: File, + logger: Logger + ): File { + val buildGradle = File(File(projectDirectory.parentFile, "app"), "build.gradle") + val buildGradleKts = File(File(projectDirectory.parentFile, "app"), "build.gradle.kts") + if (buildGradle.exists() && buildGradleKts.exists()) { + logger.error( + """ + Both build.gradle and build.gradle.kts exist, so + build.gradle.kts is ignored. This is likely a mistake. + """.trimIndent() + ) + } + + return if (buildGradle.exists()) buildGradle else buildGradleKts + } + + @JvmStatic + @JvmName("shouldProjectSplitPerAbi") + internal fun shouldProjectSplitPerAbi(project: Project): Boolean = + project + .findProperty( + PROP_SPLIT_PER_ABI + )?.toString() + ?.toBoolean() ?: false + + @JvmStatic + @JvmName("shouldProjectUseLocalEngine") + internal fun shouldProjectUseLocalEngine(project: Project): Boolean = project.hasProperty(PROP_LOCAL_ENGINE_REPO) + + @JvmStatic + @JvmName("isProjectVerbose") + internal fun isProjectVerbose(project: Project): Boolean = project.findProperty(PROP_IS_VERBOSE)?.toString()?.toBoolean() ?: false + + /** Whether to build the debug app in "fast-start" mode. */ + @JvmStatic + @JvmName("isProjectFastStart") + internal fun isProjectFastStart(project: Project): Boolean = + project + .findProperty( + PROP_IS_FAST_START + )?.toString() + ?.toBoolean() ?: false + +// TODO(gmackall): @JvmStatic internal fun getCompileSdkFromProject(project: Project): String {} + + /** + * TODO: Remove this AGP hack. https://github.com/flutter/flutter/issues/109560 + * + * In AGP 4.0, the Android linter task depends on the JAR tasks that generate `libapp.so`. + * When building APKs, this causes an issue where building release requires the debug JAR, + * but Gradle won't build debug. + * + * To workaround this issue, only configure the JAR task that is required given the task + * from the command line. + * + * The AGP team said that this issue is fixed in Gradle 7.0, which isn't released at the + * time of adding this code. Once released, this can be removed. However, after updating to + * AGP/Gradle 7.2.0/7.5, removing this hack still causes build failures. Further + * investigation necessary to remove this. + * + * Tested cases: + * * `./gradlew assembleRelease` + * * `./gradlew app:assembleRelease.` + * * `./gradlew assemble{flavorName}Release` + * * `./gradlew app:assemble{flavorName}Release` + * * `./gradlew assemble.` + * * `./gradlew app:assemble.` + * * `./gradlew bundle.` + * * `./gradlew bundleRelease.` + * * `./gradlew app:bundleRelease.` + * + * Related issues: + * https://issuetracker.google.com/issues/158060799 + * https://issuetracker.google.com/issues/158753935 + */ + @JvmStatic + @JvmName("shouldConfigureFlutterTask") + internal fun shouldConfigureFlutterTask( + project: Project, + assembleTask: Task + ): Boolean { + val cliTasksNames = project.gradle.startParameter.taskNames + if (cliTasksNames.size != 1 || !cliTasksNames.first().contains("assemble")) { + return true + } + val taskName = cliTasksNames.first().split(":").last() + if (taskName == "assemble") { + return true + } + if (taskName == assembleTask.name) { + return true + } + if (taskName.endsWith("Release") && assembleTask.name.endsWith("Release")) { + return true + } + if (taskName.endsWith("Debug") && assembleTask.name.endsWith("Debug")) { + return true + } + if (taskName.endsWith("Profile") && assembleTask.name.endsWith("Profile")) { + return true + } + return false + } + + private fun getFlutterExtensionOrNull(project: Project): FlutterExtension? = project.extensions.findByType(FlutterExtension::class.java) + + /** + * Gets the directory that contains the Flutter source code. + * This is the directory containing the `android/` directory. + */ + @JvmStatic + @JvmName("getFlutterSourceDirectory") + internal fun getFlutterSourceDirectory(project: Project): File { + val flutterExtension = getFlutterExtensionOrNull(project) + // TODO(gmackall): clean up this NPE that is still around from the Groovy conversion. + if (flutterExtension!!.source == null) { + throw GradleException("Flutter source directory not set.") + } + return project.file(flutterExtension.source!!) + } + + /** + * Gets the target file. This is typically `lib/main.dart`. + * + * Returns + * 1. the value of the `target` property, if it exists + * 2. the target value set in the FlutterExtension, if it exists + * 3. `lib/main.dart` otherwise + */ + @JvmStatic + @JvmName("getFlutterTarget") + internal fun getFlutterTarget(project: Project): String { + if (project.hasProperty(PROP_TARGET)) { + return project.property(PROP_TARGET).toString() + } + val target: String = getFlutterExtensionOrNull(project)!!.target ?: "lib/main.dart" + return target + } + + @JvmStatic + @JvmName("isBuiltAsApp") + internal fun isBuiltAsApp(project: Project): Boolean { + // Projects are built as applications when the they use the `com.android.application` + // plugin. + return project.plugins.hasPlugin("com.android.application") + } + + // Optional parameters don't work when Groovy makes calls into Kotlin, so provide an additional + // signature for the 3 argument version. + @JvmStatic + @JvmName("addApiDependencies") + internal fun addApiDependencies( + project: Project, + variantName: String, + dependency: Any + ) { + addApiDependencies(project, variantName, dependency, null) + } + + @JvmStatic + @JvmName("addApiDependencies") + internal fun addApiDependencies( + project: Project, + variantName: String, + dependency: Any, + config: Closure? + ) { + var configuration: String + try { + project.configurations.named("api") + configuration = "${variantName}Api" + } catch (ignored: UnknownTaskException) { + // TODO(gmackall): The docs say the above should actually be an UnknownDomainObjectException. + configuration = "${variantName}Compile" + } + + if (config == null) { + project.dependencies.add( + configuration, + dependency + ) + } else { + project.dependencies.add(configuration, dependency, config) + } + } + + /** + * Returns a Flutter build mode suitable for the specified Android buildType. + * + * @return "debug", "profile", or "release" (fall-back). + */ + @JvmStatic + @JvmName("buildModeFor") + internal fun buildModeFor(buildType: BuildType): String { + if (buildType.name == "profile") { + return "profile" + } else if (buildType.isDebuggable) { + return "debug" + } + return "release" + } + + /** + * Returns true if the build mode is supported by the current call to Gradle. + * This only relevant when using a local engine. Because the engine + * is built for a specific mode, the call to Gradle must match that mode. + */ + @JvmStatic + @JvmName("supportsBuildMode") + internal fun supportsBuildMode( + project: Project, + flutterBuildMode: String + ): Boolean { + if (!shouldProjectUseLocalEngine(project)) { + return true + } + check(project.hasProperty(PROP_LOCAL_ENGINE_BUILD_MODE)) { "Project must have property '$PROP_LOCAL_ENGINE_BUILD_MODE'" } + // Don't configure dependencies for a build mode that the local engine + // doesn't support. + return project.property(PROP_LOCAL_ENGINE_BUILD_MODE) == flutterBuildMode + } +} diff --git a/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt new file mode 100644 index 0000000000..aba45a5882 --- /dev/null +++ b/packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginUtilsTest.kt @@ -0,0 +1,411 @@ +package com.flutter.gradle + +import com.android.builder.model.BuildType +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.UnknownTaskException +import org.gradle.api.logging.Logger +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.nio.file.Path +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class FlutterPluginUtilsTest { + // toCamelCase + @Test + fun `toCamelCase converts a list of strings to camel case`() { + val result = FlutterPluginUtils.toCamelCase(listOf("hello", "world")) + assertEquals("helloWorld", result) + } + + @Test + fun `toCamelCase handles empty list`() { + val result = FlutterPluginUtils.toCamelCase(emptyList()) + assertEquals("", result) + } + + @Test + fun `toCamelCase handles single-element list`() { + val result = FlutterPluginUtils.toCamelCase(listOf("hello")) + assertEquals("hello", result) + } + + // compareVersionStrings + @Test + fun `compareVersionStrings compares last element of version string correctly`() { + val result = FlutterPluginUtils.compareVersionStrings("1.2.3", "1.2.4") + assertEquals(-1, result) + } + + @Test + fun `compareVersionStrings compares middle element of version string correctly`() { + val result = FlutterPluginUtils.compareVersionStrings("1.2.3", "1.1.4") + assertEquals(1, result) + } + + @Test + fun `compareVersionStrings compares first element of version string correctly`() { + val result = FlutterPluginUtils.compareVersionStrings("1.2.3", "2.2.4") + assertEquals(-1, result) + } + + @Test + fun `compareVersionStrings considers rc candidates the same`() { + val result = FlutterPluginUtils.compareVersionStrings("1.2.3-rc", "1.2.3") + assertEquals(0, result) + } + + // shouldShrinkResources + @Test + fun `shouldShrinkResources returns true by default`() { + val project = mockk() + every { project.hasProperty(any()) } returns false + val result = FlutterPluginUtils.shouldShrinkResources(project) + assertEquals(true, result) + } + + @Test + fun `shouldShrinkResources returns true when property is set to true`() { + val project = mockk() + every { project.hasProperty(FlutterPluginUtils.PROP_SHOULD_SHRINK_RESOURCES) } returns true + every { project.property(FlutterPluginUtils.PROP_SHOULD_SHRINK_RESOURCES) } returns true + val result = FlutterPluginUtils.shouldShrinkResources(project) + assertEquals(true, result) + } + + // pluginSupportsAndroidPlatform + @Test + fun `pluginSupportsAndroidPlatform returns true when android directory exists with gradle build file`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("my-plugin") + projectDir.toFile().mkdirs() + + val androidDir = tempDir.resolve("android") + androidDir.toFile().mkdirs() + File(androidDir.toFile(), "build.gradle").createNewFile() + + val mockProject = + mockk { + every { this@mockk.projectDir } returns projectDir.toFile() + } + + assertTrue { + FlutterPluginUtils.pluginSupportsAndroidPlatform(mockProject) + } // Replace YourClass with the actual class containing the method + } + + @Test + fun `pluginSupportsAndroidPlatform returns false when gradle build file does not exist`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("my-plugin") + projectDir.toFile().mkdirs() + + val mockProject = + mockk { + every { this@mockk.projectDir } returns projectDir.toFile() + } + + assertFalse { + FlutterPluginUtils.pluginSupportsAndroidPlatform(mockProject) + } // Replace YourClass with the actual class containing the method + } + + // settingsGradleFile + @Test + fun `settingsGradleFile returns groovy settings gradle file when it exists`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("android").resolve("app") + projectDir.toFile().mkdirs() + + val settingsGradle = File(projectDir.parent.toFile(), "settings.gradle") + settingsGradle.createNewFile() + + val result = FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockk()) + assertEquals(settingsGradle, result) + } + + @Test + fun `settingsGradleFile returns groovy settings file and logs when both groovy and kotlin exist`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("android").resolve("app") + projectDir.toFile().mkdirs() + + val groovySettingsGradle = File(projectDir.parent.toFile(), "settings.gradle") + groovySettingsGradle.createNewFile() + val kotlinSettingsGradle = File(projectDir.parent.toFile(), "settings.gradle.kts") + kotlinSettingsGradle.createNewFile() + + val mockLogger = mockk() + every { mockLogger.error(any()) } returns Unit + + val result = FlutterPluginUtils.getSettingsGradleFileFromProjectDir(projectDir.toFile(), mockLogger) + assertEquals(groovySettingsGradle, result) + verify { mockLogger.error(any()) } + } + + // buildGradleFile + @Test + fun `buildGradleFile returns groovy build gradle file when it exists`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("android").resolve("app") + projectDir.toFile().mkdirs() + + val buildGradle = File(projectDir.parent.resolve("app").toFile(), "build.gradle") + buildGradle.createNewFile() + + val result = FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockk()) + assertEquals(buildGradle, result) + } + + @Test + fun `buildGradleFile returns groovy build file and logs when both groovy and kotlin exist`( + @TempDir tempDir: Path + ) { + val projectDir = tempDir.resolve("android").resolve("app") + projectDir.toFile().mkdirs() + + val groovyBuildGradle = File(projectDir.parent.resolve("app").toFile(), "build.gradle") + groovyBuildGradle.createNewFile() + val kotlinBuildGradle = File(projectDir.parent.resolve("app").toFile(), "build.gradle.kts") + kotlinBuildGradle.createNewFile() + + val mockLogger = mockk() + every { mockLogger.error(any()) } returns Unit + + val result = FlutterPluginUtils.getBuildGradleFileFromProjectDir(projectDir.toFile(), mockLogger) + assertEquals(groovyBuildGradle, result) + verify { mockLogger.error(any()) } + } + + // shouldProjectSplitPerAbi + @Test + fun `shouldProjectSplitPerAbi returns false by default`() { + val project = mockk() + every { project.findProperty(FlutterPluginUtils.PROP_SPLIT_PER_ABI) } returns null + val result = FlutterPluginUtils.shouldProjectSplitPerAbi(project) + assertEquals(false, result) + } + + @Test + fun `shouldProjectSplitPerAbi returns true when property is set to true`() { + val project = mockk() + every { project.findProperty(FlutterPluginUtils.PROP_SPLIT_PER_ABI) } returns "true" + val result = FlutterPluginUtils.shouldProjectSplitPerAbi(project) + assertEquals(true, result) + } + + // shouldProjectUseLocalEngine skipped as it is a wrapper for a single getter + + // isProjectVerbose + @Test + fun `isProjectVerbose returns false by default`() { + val project = mockk() + every { project.findProperty(FlutterPluginUtils.PROP_IS_VERBOSE) } returns null + val result = FlutterPluginUtils.isProjectVerbose(project) + assertEquals(false, result) + } + + // isProjectVerbose + @Test + fun `isProjectVerbose returns true when the property is set to true`() { + val project = mockk() + every { project.findProperty(FlutterPluginUtils.PROP_IS_VERBOSE) } returns true + val result = FlutterPluginUtils.isProjectVerbose(project) + assertEquals(true, result) + } + + // isProjectFastStart + @Test + fun `isProjectFastStart returns false by default`() { + val project = mockk() + every { project.findProperty(FlutterPluginUtils.PROP_IS_FAST_START) } returns null + val result = FlutterPluginUtils.isProjectFastStart(project) + assertEquals(false, result) + } + + @Test + fun `isProjectFastStart returns true when the property is set to true`() { + val project = mockk() + every { project.findProperty(FlutterPluginUtils.PROP_IS_FAST_START) } returns true + val result = FlutterPluginUtils.isProjectFastStart(project) + assertEquals(true, result) + } + + // shouldConfigureFlutterTask + @Test + fun `shouldConfigureFlutterTask returns true for assemble task`() { + val project = mockk() + val assembleTask = mockk() + + every { project.gradle.startParameter.taskNames } returns listOf("assemble") + + val result = FlutterPluginUtils.shouldConfigureFlutterTask(project, assembleTask) + assertEquals(true, result) + } + + @Test + fun `shouldConfigureFlutterTask returns true when taskname and assembleTask end with Release`() { + val project = mockk() + val assembleTask = mockk() + + every { project.gradle.startParameter.taskNames } returns listOf("assembleRelease") + every { assembleTask.name } returns "assembleSomethingElseRelease" + + val result = FlutterPluginUtils.shouldConfigureFlutterTask(project, assembleTask) + assertEquals(true, result) + } + + // getFlutterSourceDirectory + @Test + fun `getFlutterSourceDirectory returns the flutter source directory`() { + val flutterExtension = FlutterExtension() + val project = mockk() + + flutterExtension.source = "my/flutter/source/directory" + every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension + every { project.file(any()) } returns mockk() + + FlutterPluginUtils.getFlutterSourceDirectory(project) + verify { project.file("my/flutter/source/directory") } + } + + @Test + fun `getFlutterSourceDirectory throws exception when flutter source directory is not set`() { + val flutterExtension = FlutterExtension() + val project = mockk() + + flutterExtension.source = null + every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension + + assertThrows { + FlutterPluginUtils.getFlutterSourceDirectory(project) + } + } + + // getFlutterTarget + @Test + fun `getFlutterTarget returns the target when the project property is set`() { + val project = mockk() + every { project.hasProperty(FlutterPluginUtils.PROP_TARGET) } returns true + every { project.property(FlutterPluginUtils.PROP_TARGET) } returns "my/target" + + val result = FlutterPluginUtils.getFlutterTarget(project) + assertEquals("my/target", result) + } + + @Test + fun `getFlutterTarget returns the target from the FlutterExtension when it is set and project property is not set`() { + val flutterExtension = FlutterExtension() + val project = mockk() + flutterExtension.target = "my/target" + every { project.hasProperty(FlutterPluginUtils.PROP_TARGET) } returns false + every { project.extensions.findByType(FlutterExtension::class.java) } returns flutterExtension + + val result = FlutterPluginUtils.getFlutterTarget(project) + assertEquals(flutterExtension.target, result) + } + + @Test + fun `getFlutterTarget returns the default target when it is not set`() { + val project = mockk() + every { project.hasProperty(FlutterPluginUtils.PROP_TARGET) } returns false + every { project.extensions.findByType(FlutterExtension::class.java)!!.target } returns null + + val result = FlutterPluginUtils.getFlutterTarget(project) + assertEquals("lib/main.dart", result) + } + + // isBuiltAsApp skipped as it is a wrapper for a single getter + + // addApiDependencies + @Test + fun `addApiDependencies adds the dependency with the correct name when no UnknownTaskException`() { + val project = mockk() + val variantName = "debug" + val dependency = mockk() + + every { project.configurations.named("api") } returns mockk() + every { project.dependencies.add(any(), any()) } returns mockk() + + FlutterPluginUtils.addApiDependencies(project, variantName, dependency) + + verify { project.dependencies.add("debugApi", dependency) } + } + + @Test + fun `addApiDependencies adds the dependency with the correct name when UnknownTaskException`() { + val project = mockk() + val variantName = "debug" + val dependency = mockk() + + every { project.configurations.named("api") } throws UnknownTaskException("message", mockk()) + every { project.dependencies.add(any(), any()) } returns mockk() + + FlutterPluginUtils.addApiDependencies(project, variantName, dependency) + + verify { project.dependencies.add("debugCompile", dependency) } + } + + // buildModeFor + @Test + fun `buildModeFor returns profile if the BuildType has name profile`() { + val buildType = mockk() + every { buildType.name } returns "profile" + + val result = FlutterPluginUtils.buildModeFor(buildType) + assertEquals("profile", result) + } + + @Test + fun `buildModeFor returns debug if the BuildType is debuggable`() { + val buildType = mockk() + every { buildType.name } returns "something random" + every { buildType.isDebuggable } returns true + + val result = FlutterPluginUtils.buildModeFor(buildType) + assertEquals("debug", result) + } + + @Test + fun `buildModeFor returns release if the BuildType is not debuggable and not named profile`() { + val buildType = mockk() + every { buildType.isDebuggable } returns false + every { buildType.name } returns "something random" + + val result = FlutterPluginUtils.buildModeFor(buildType) + assertEquals("release", result) + } + + // supportsBuildMode + @Test + fun `supportsBuildMode returns true if project should not use local engine`() { + val project = mockk() + every { project.hasProperty(FlutterPluginUtils.PROP_LOCAL_ENGINE_REPO) } returns false + val result = FlutterPluginUtils.supportsBuildMode(project, "debug") + assertEquals(true, result) + } + + @Test + fun `supportsBuildMode returns false if project should use local engine and build mode does not match`() { + val project = mockk() + every { project.hasProperty(FlutterPluginUtils.PROP_LOCAL_ENGINE_REPO) } returns true + every { project.hasProperty(FlutterPluginUtils.PROP_LOCAL_ENGINE_BUILD_MODE) } returns true + every { project.property(FlutterPluginUtils.PROP_LOCAL_ENGINE_BUILD_MODE) } returns "debug" + + val result = FlutterPluginUtils.supportsBuildMode(project, "release") + assertEquals(false, result) + } +}