| // Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:io'; | |
| import 'package:jni_util/jni_util.dart'; | |
| import 'package:path/path.dart'; | |
| import 'package:yaml/yaml.dart'; | |
| import '../logging/logging.dart'; | |
| class _AndroidToolsException implements Exception { | |
| _AndroidToolsException(this.message); | |
| /// Error message describing the failure. | |
| String message; | |
| @override | |
| String toString() => message; | |
| } | |
| /// Exception thrown when the Android SDK cannot be found. | |
| class SdkNotFoundException extends _AndroidToolsException { | |
| SdkNotFoundException(super.message); | |
| } | |
| /// Exception thrown when a Gradle operation fails. | |
| class GradleException extends _AndroidToolsException { | |
| GradleException(super.message); | |
| } | |
| /// Utilities for locating Android SDK components and running Gradle stubs. | |
| class AndroidSdkTools { | |
| /// Locates and returns the Android SDK root directory from the | |
| /// `ANDROID_SDK_ROOT` environment variable. | |
| static Uri getAndroidSdkRoot() { | |
| final envVar = Platform.environment['ANDROID_SDK_ROOT']; | |
| if (envVar == null) { | |
| throw SdkNotFoundException('Android SDK not found. Please set ' | |
| 'ANDROID_SDK_ROOT environment variable or specify through command ' | |
| 'line override.'); | |
| } | |
| return Uri.directory(envVar); | |
| } | |
| static Future<String?> _getVersionDir( | |
| String relative, Uri sdkRoot, List<int> versionOrder) async { | |
| final parent = join(sdkRoot.toFilePath(), relative); | |
| for (var version in versionOrder) { | |
| final dir = Directory(join(parent, 'android-$version')); | |
| if (await dir.exists()) { | |
| return dir.path; | |
| } | |
| } | |
| return null; | |
| } | |
| static Future<Uri?> _getFile( | |
| Uri sdkRoot, String relative, List<int> versionOrder, String file) async { | |
| final platform = await _getVersionDir(relative, sdkRoot, versionOrder); | |
| if (platform == null) return null; | |
| final filePath = join(platform, file); | |
| if (await File(filePath).exists()) { | |
| log.info('Found $filePath'); | |
| return Uri.file(filePath); | |
| } | |
| return null; | |
| } | |
| static const _gradleCannotFindJars = 'Gradle stub cannot find JAR libraries.'; | |
| static const _leftOverStubWarning = 'If you are seeing this error in ' | |
| '`flutter build` output, it is likely that JNIgen left some stubs in ' | |
| 'the build.gradle file. Please restore that file from your version ' | |
| 'control system or manually remove the stub functions named ' | |
| '$_gradleGetClasspathTaskName and / or $_gradleGetSourcesTaskName.'; | |
| /// Finds the path to `android.jar` in [sdkRoot] according to [versionOrder]. | |
| static Future<Uri?> getAndroidJarPath( | |
| {required Uri sdkRoot, required List<int> versionOrder}) async => | |
| await _getFile(sdkRoot, 'platforms', versionOrder, 'android.jar'); | |
| static const _gradleGetClasspathTaskName = 'getReleaseCompileClasspath'; | |
| static const _gradleGetClasspathStub = ''' | |
| // Gradle stub for listing dependencies in JNIgen. If found in | |
| // android/build.gradle, please delete the following task. | |
| tasks.register("$_gradleGetClasspathTaskName") { | |
| // Tell Gradle to complete the configuration phase of all subprojects (eg | |
| // Flutter plugins) before continuing configuration of this project. | |
| allprojects { project -> | |
| if (project != rootProject) { | |
| evaluationDependsOn(project.path) | |
| } | |
| } | |
| // Fetch all the dependencies and extract the JARs. | |
| allprojects { project -> | |
| def config = project.configurations.findByName("releaseCompileClasspath") | |
| if (config == null) return | |
| try { | |
| // Find all JARs. | |
| def jarView = config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String.class), "jar") | |
| } | |
| lenient = true | |
| } | |
| inputs.files(jarView.files) | |
| // Also find all JARs stored in AARs. | |
| def aarView = config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String.class), "android-classes-jar") | |
| } | |
| lenient = true | |
| } | |
| inputs.files(aarView.files) | |
| } catch (Exception e) {} | |
| } | |
| // Find all the JARs and print their paths. | |
| doLast { | |
| try { | |
| def cp = [] | |
| // JNIgen uses the first version of a class it finds, so the order we list | |
| // the JARs in is important. | |
| def mainProject = allprojects.find { project -> | |
| project.plugins.hasPlugin("com.android.application") | |
| } ?: project | |
| // Start with the android bootClasspath. This contains things like java.* | |
| // and android.*. | |
| def android = mainProject.extensions.findByName("android") | |
| if (android != null && android.hasProperty("bootClasspath")) { | |
| cp.addAll(android.bootClasspath) | |
| } | |
| // Next, add the main project's JARs, followed by all the other project's | |
| // JARs. | |
| def projects = ([mainProject] + allprojects).unique() | |
| projects.each { project -> | |
| def config = project.configurations.findByName("releaseCompileClasspath") | |
| if (config == null) return | |
| try { | |
| // Add all JARs. | |
| cp += config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String.class), "jar") | |
| } | |
| lenient = true | |
| }.files | |
| // Add all JARs that were contained in AARs. | |
| cp += config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String.class), "android-classes-jar") | |
| } | |
| lenient = true | |
| }.files | |
| } catch (Exception e) {} | |
| } | |
| // Dedupe and print the absolute paths to all the JARs. | |
| cp.collect { file -> file.absolutePath }.unique().each { path -> | |
| println path | |
| } | |
| } catch (Exception e) { | |
| System.err.println("$_gradleCannotFindJars") | |
| throw e | |
| } | |
| } | |
| System.err.println("$_leftOverStubWarning") | |
| } | |
| '''; | |
| static const _gradleGetClasspathStubKt = ''' | |
| // Gradle stub for listing dependencies in JNIgen. If found in | |
| // android/build.gradle.kts, please delete the following task. | |
| tasks.register<DefaultTask>("$_gradleGetClasspathTaskName") { | |
| // Tell Gradle to complete the configuration phase of all subprojects (eg | |
| // Flutter plugins) before continuing configuration of this project. | |
| allprojects { | |
| if (this != rootProject) { | |
| evaluationDependsOn(path) | |
| } | |
| } | |
| // Fetch all the dependencies and extract the JARs. | |
| allprojects { | |
| val config = configurations.findByName("releaseCompileClasspath") ?: return@allprojects | |
| try { | |
| // Find all JARs. | |
| val jarView = config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String::class.java), "jar") | |
| } | |
| lenient(true) | |
| } | |
| inputs.files(jarView.files) | |
| // Also find all JARs stored in AARs. | |
| val aarView = config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String::class.java), "android-classes-jar") | |
| } | |
| lenient(true) | |
| } | |
| inputs.files(aarView.files) | |
| } catch (e: Exception) {} | |
| } | |
| // Find all the JARs and print their paths. | |
| doLast { | |
| try { | |
| val cp = mutableListOf<File>() | |
| // JNIgen uses the first version of a class it finds, so the order we list | |
| // the JARs in is important. | |
| val mainProject = allprojects.find { project -> | |
| project.plugins.hasPlugin("com.android.application") | |
| } ?: project | |
| // Start with the android bootClasspath. This contains things like java.* | |
| // and android.*. | |
| val android = mainProject.extensions.findByName("android") as? com.android.build.gradle.BaseExtension | |
| android?.bootClasspath?.let { file -> cp.addAll(file) } | |
| // Next, add the main project's JARs, followed by all the other project's | |
| // JARs. | |
| val projects = (listOf(mainProject) + allprojects).distinct() | |
| projects.forEach { project -> | |
| val config = project.configurations.findByName("releaseCompileClasspath") ?: return@forEach | |
| try { | |
| // Add all JARs. | |
| cp.addAll(config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String::class.java), "jar") | |
| } | |
| lenient(true) | |
| }.files) | |
| // Add all JARs that were contained in AARs. | |
| cp.addAll(config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String::class.java), "android-classes-jar") | |
| } | |
| lenient(true) | |
| }.files) | |
| } catch (e: Exception) {} | |
| } | |
| // Dedupe and print the absolute paths to all the JARs. | |
| cp.map { file -> file.absolutePath }.distinct().forEach { | |
| path -> println(path) | |
| } | |
| } catch (e: Exception) { | |
| System.err.println("$_gradleCannotFindJars") | |
| throw e | |
| } | |
| } | |
| System.err.println("$_leftOverStubWarning") | |
| } | |
| '''; | |
| static const _gradleGetSourcesTaskName = 'getSources'; | |
| static const _gradleGetSourcesStub = ''' | |
| // Gradle stub for fetching source dependencies in JNIgen. If found in | |
| // android/build.gradle, please delete the following task. | |
| tasks.register("$_gradleGetSourcesTaskName") { | |
| // Tell Gradle to complete the configuration phase of all subprojects (eg | |
| // Flutter plugins) before continuing configuration of this project. | |
| allprojects { project -> | |
| if (project != rootProject) { | |
| evaluationDependsOn(project.path) | |
| } | |
| } | |
| // Fetch all the dependencies and extract the JARs. | |
| allprojects { project -> | |
| def config = project.configurations.findByName("releaseCompileClasspath") | |
| if (config == null) return | |
| try { | |
| // Find all JARs. | |
| def jarView = config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String.class), "jar") | |
| } | |
| lenient = true | |
| } | |
| inputs.files(jarView.files) | |
| } catch (Exception e) {} | |
| } | |
| // Find all the sources and print their paths. | |
| doLast { | |
| // JNIgen uses the first version of a class it finds, so the order we list | |
| // the sources in is important. So list the main project's sources before | |
| // the other projects. | |
| def mainProject = allprojects.find { project -> | |
| project.plugins.hasPlugin("com.android.application") | |
| } ?: project | |
| def projects = ([mainProject] + allprojects).unique() | |
| projects.each { project -> | |
| def config = project.configurations.findByName("releaseCompileClasspath") | |
| if (config == null) return | |
| // Find all dependency artifacts. | |
| def componentIds = config.incoming.resolutionResult.allDependencies.collect { dependency -> | |
| dependency.selected.id | |
| } | |
| ArtifactResolutionResult result = dependencies.createArtifactResolutionQuery() | |
| .forComponents(componentIds) | |
| .withArtifacts(JvmLibrary, SourcesArtifact) | |
| .execute() | |
| // Find the sources in each artifact. | |
| def sourceArtifacts = [] | |
| result.resolvedComponents.each { ComponentArtifactsResult component -> | |
| Set<ArtifactResult> sources = component.getArtifacts(SourcesArtifact) | |
| sources.each { ArtifactResult artifactResult -> | |
| if (artifactResult instanceof ResolvedArtifactResult) { | |
| sourceArtifacts << artifactResult.file | |
| } | |
| } | |
| } | |
| // Dedupe and print the absolute paths to all the sources. | |
| sourceArtifacts.collect { file -> file.absolutePath }.unique().each { path -> | |
| println path | |
| } | |
| } | |
| } | |
| System.err.println("$_leftOverStubWarning") | |
| } | |
| '''; | |
| static const _gradleGetSourcesStubKt = ''' | |
| // Gradle stub for fetching source dependencies in JNIgen. If found in | |
| // android/build.gradle.kts, please delete the following task. | |
| tasks.register<DefaultTask>("$_gradleGetSourcesTaskName") { | |
| // Tell Gradle to complete the configuration phase of all subprojects (eg | |
| // Flutter plugins) before continuing configuration of this project. | |
| allprojects { | |
| if (this != rootProject) { | |
| evaluationDependsOn(path) | |
| } | |
| } | |
| // Fetch all the dependencies and extract the JARs. | |
| allprojects { | |
| val config = configurations.findByName("releaseCompileClasspath") ?: return@allprojects | |
| try { | |
| // Find all JARs. | |
| val jarView = config.incoming.artifactView { | |
| attributes { | |
| attribute(org.gradle.api.attributes.Attribute.of("artifactType", String::class.java), "jar") | |
| } | |
| lenient(true) | |
| } | |
| inputs.files(jarView.files) | |
| } catch (e: Exception) {} | |
| } | |
| // Find all the sources and print their paths. | |
| doLast { | |
| // JNIgen uses the first version of a class it finds, so the order we list | |
| // the sources in is important. So list the main project's sources before | |
| // the other projects. | |
| val mainProject = allprojects.find { project -> | |
| project.plugins.hasPlugin("com.android.application") | |
| } ?: project | |
| val projects = (listOf(mainProject) + allprojects).distinct() | |
| projects.forEach { project -> | |
| val config = project.configurations.findByName("releaseCompileClasspath") | |
| if (config == null) return@forEach | |
| // Find all dependency artifacts. | |
| val componentIds = | |
| config.incoming.resolutionResult.allDependencies | |
| .filterIsInstance<org.gradle.api.artifacts.result.ResolvedDependencyResult>() | |
| .map { dependency -> dependency.selected.id } | |
| val result = dependencies.createArtifactResolutionQuery() | |
| .forComponents(componentIds) | |
| .withArtifacts(org.gradle.api.artifacts.result.JvmLibrary::class, org.gradle.api.artifacts.result.SourcesArtifact::class) | |
| .execute() | |
| val sourceArtifacts = mutableListOf<File>() | |
| for (component in result.resolvedComponents) { | |
| val sourcesArtifactsResult = component.getArtifacts(org.gradle.api.artifacts.result.SourcesArtifact::class) | |
| for (artifactResult in sourcesArtifactsResult) { | |
| if (artifactResult is org.gradle.api.artifacts.result.ResolvedArtifactResult) { | |
| sourceArtifacts.add(artifactResult.file) | |
| } | |
| } | |
| } | |
| // Dedupe and print the absolute paths to all the sources. | |
| sourceArtifacts.map { file -> file.absolutePath }.distinct().forEach { | |
| path -> println(path) | |
| } | |
| } | |
| } | |
| System.err.println("$_leftOverStubWarning") | |
| } | |
| '''; | |
| /// Get release compile classpath used by Gradle for android build. | |
| /// | |
| /// This function temporarily overwrites the build.gradle file by a stub with | |
| /// function to list all dependency paths for release variant. | |
| /// This function fails if no gradle build is attempted before. | |
| /// | |
| /// If current project is not directly buildable by gradle, eg: a plugin, | |
| /// a relative path to other project can be specified using [androidProject]. | |
| static List<String> getGradleClasspaths({Uri? androidProject}) => | |
| _runGradleStub( | |
| isSource: false, | |
| androidProject: androidProject ?? Uri.directory('.'), | |
| ); | |
| /// Get source paths for all gradle dependencies. | |
| /// | |
| /// This function temporarily overwrites the build.gradle file by a stub with | |
| /// function to list all dependency paths for release variant. | |
| /// This function fails if no gradle build is attempted before. | |
| static List<String> getGradleSources({Uri? androidProject}) { | |
| return _runGradleStub( | |
| isSource: true, | |
| androidProject: androidProject ?? Uri.directory('.'), | |
| ); | |
| } | |
| static String _appendStub(String script, String stub) { | |
| return script.contains(stub) ? script : script + stub; | |
| } | |
| static String _inAndroidProject(String androidProject) => | |
| (androidProject == '') ? '' : ' in $androidProject'; | |
| static List<String> _runGradleStub({ | |
| required bool isSource, | |
| Uri? androidProject, | |
| }) { | |
| final stubName = | |
| isSource ? _gradleGetSourcesTaskName : _gradleGetClasspathTaskName; | |
| log.info('trying to obtain gradle dependencies [$stubName]...'); | |
| final androidProjectUri = androidProject ?? Uri.directory('.'); | |
| final androidProjectPath = androidProjectUri.toFilePath(); | |
| if (_isFlutterProject(androidProjectPath)) { | |
| _runFlutterConfigOnly(androidProjectPath); | |
| } | |
| final android = join(androidProjectPath, 'android'); | |
| var buildGradle = join(android, 'build.gradle'); | |
| final usesKotlinScript = !File.fromUri(Uri.file(buildGradle)).existsSync(); | |
| if (usesKotlinScript) { | |
| // The Kotlin version of the script is injected to `app/build.gradle.kts` | |
| // instead of `build.gradle`. | |
| buildGradle = join(android, 'app', 'build.gradle.kts'); | |
| } | |
| final String stubCode; | |
| if (isSource) { | |
| stubCode = | |
| usesKotlinScript ? _gradleGetSourcesStubKt : _gradleGetSourcesStub; | |
| } else { | |
| stubCode = usesKotlinScript | |
| ? _gradleGetClasspathStubKt | |
| : _gradleGetClasspathStub; | |
| } | |
| final buildGradleOld = '$buildGradle.old'; | |
| final origBuild = File(buildGradle); | |
| final script = origBuild.readAsStringSync(); | |
| origBuild.renameSync(buildGradleOld); | |
| origBuild.createSync(); | |
| log.finer('Writing temporary gradle script with stub "$stubName"...'); | |
| origBuild.writeAsStringSync(_appendStub(script, stubCode)); | |
| log.info('Running gradle wrapper...'); | |
| final gradleCommand = Platform.isWindows ? '.\\gradlew.bat' : './gradlew'; | |
| final taskPath = usesKotlinScript ? ':app:$stubName' : stubName; | |
| ProcessResult procRes; | |
| try { | |
| procRes = Process.runSync(gradleCommand, ['-q', taskPath], | |
| workingDirectory: android, | |
| runInShell: true, | |
| environment: javaEnvironment); | |
| } finally { | |
| log.info('Restoring build scripts'); | |
| origBuild.writeAsStringSync( | |
| script | |
| .replaceAll(_gradleGetClasspathStub, '') | |
| .replaceAll(_gradleGetSourcesStub, ''), | |
| ); | |
| if (File(buildGradleOld).existsSync()) { | |
| File(buildGradleOld).deleteSync(); | |
| } | |
| } | |
| if (procRes.exitCode != 0) { | |
| final inAndroidProject = _inAndroidProject(androidProjectPath); | |
| throw GradleException('''\n\nGradle execution failed. | |
| 1. The most likely cause is that the Flutter metadata files are not yet cached. | |
| Run `flutter pub get`$inAndroidProject and try again. | |
| 2. If the Gradle output says "./gradlew: not found", the Flutter Gradle wrapper | |
| script is missing. | |
| Run `flutter build apk --config-only`$inAndroidProject and try again. | |
| This is commonly seen in a fresh clone of a git repo. You can also consider | |
| adjusting your .gitignore rules to check these files in to your git repo. | |
| 3. If the Gradle output includes text like this: | |
| * What went wrong: | |
| Execution failed for task ':gradle:compileGroovy'. | |
| > BUG! exception in phase 'semantic analysis' ... Unsupported class file major version | |
| Then the JDK versions used by JNIgen and Flutter are not compatible. Try | |
| changing the default JDK version e.g. with `export JAVA_VERSION=$minJavaVersion` | |
| on macOS and `sudo update-alternatives --config java` on Ubuntu. | |
| GRADLE OUTPUT: | |
| -------------------------------------------------------------------------------- | |
| ${procRes.stderr} | |
| -------------------------------------------------------------------------------- | |
| '''); | |
| } | |
| // Record both stdout and stderr of gradle. | |
| log.writeSectionToFile('Gradle logs ($stubName)', procRes.stderr); | |
| log.writeSectionToFile('Gradle output ($stubName)', procRes.stdout); | |
| final output = procRes.stdout as String; | |
| if (output.isEmpty) { | |
| printError(procRes.stderr); | |
| throw Exception('Gradle stub exited without output.'); | |
| } | |
| final paths = (procRes.stdout as String) | |
| .trim() | |
| .split(Platform.isWindows ? '\r\n' : '\n'); | |
| log.fine('Found ${paths.length} entries'); | |
| return paths; | |
| } | |
| static bool _isFlutterProject(String projectPath) { | |
| try { | |
| final pubspecFile = File(join(projectPath, 'pubspec.yaml')); | |
| final pubspec = loadYaml(pubspecFile.readAsStringSync()); | |
| return pubspec is Map && pubspec.containsKey('flutter'); | |
| } catch (e) { | |
| return false; | |
| } | |
| } | |
| static void _runFlutterConfigOnly(String androidProject) { | |
| final inAndroidProject = _inAndroidProject(androidProject); | |
| log.info('Running "flutter build apk --config-only"$inAndroidProject...'); | |
| var success = false; | |
| try { | |
| final result = Process.runSync( | |
| 'flutter', | |
| ['build', 'apk', '--config-only'], | |
| workingDirectory: androidProject, | |
| runInShell: true, | |
| environment: javaEnvironment, | |
| ); | |
| if (result.exitCode == 0) { | |
| success = true; | |
| } else { | |
| log.warning(result.stderr); | |
| } | |
| } catch (e) { | |
| log.warning('Error: $e'); | |
| } | |
| if (!success) { | |
| log.warning( | |
| 'Flutter configuration failed. Failure may be legitimate if this ' | |
| 'project uses a non-standard structure, so attempting to continue ' | |
| 'with JNIgen anyway...', | |
| ); | |
| } | |
| } | |
| } |