Check and give execute permission to Gradle if needed (#46748)
diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart index b6c26d8..7aef19c 100644 --- a/packages/flutter_tools/lib/src/android/gradle_utils.dart +++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart
@@ -46,14 +46,17 @@ final Directory androidDir = project.android.hostAppGradleRoot; // Update the project if needed. // TODO(egarciad): https://github.com/flutter/flutter/issues/40460 - migrateToR8(androidDir); - injectGradleWrapperIfNeeded(androidDir); + gradleUtils.migrateToR8(androidDir); + gradleUtils.injectGradleWrapperIfNeeded(androidDir); final File gradle = androidDir.childFile( platform.isWindows ? 'gradlew.bat' : 'gradlew', ); if (gradle.existsSync()) { printTrace('Using gradle from ${gradle.absolute.path}.'); + // If the Gradle executable doesn't have execute permission, + // then attempt to set it. + _giveExecutePermissionIfNeeded(gradle); return gradle.absolute.path; } throwToolExit( @@ -62,71 +65,69 @@ ); return null; } -} -/// Migrates the Android's [directory] to R8. -/// https://developer.android.com/studio/build/shrink-code -@visibleForTesting -void migrateToR8(Directory directory) { - final File gradleProperties = directory.childFile('gradle.properties'); - if (!gradleProperties.existsSync()) { - throwToolExit( - 'Expected file ${gradleProperties.path}. ' - 'Please ensure that this file exists or that ${gradleProperties.dirname} can be read.' - ); - } - final String propertiesContent = gradleProperties.readAsStringSync(); - if (propertiesContent.contains('android.enableR8')) { - printTrace('gradle.properties already sets `android.enableR8`'); - return; - } - printTrace('set `android.enableR8=true` in gradle.properties'); - try { - if (propertiesContent.isNotEmpty && !propertiesContent.endsWith('\n')) { - // Add a new line if the file doesn't end with a new line. - gradleProperties.writeAsStringSync('\n', mode: FileMode.append); + /// Migrates the Android's [directory] to R8. + /// https://developer.android.com/studio/build/shrink-code + @visibleForTesting + void migrateToR8(Directory directory) { + final File gradleProperties = directory.childFile('gradle.properties'); + if (!gradleProperties.existsSync()) { + throwToolExit( + 'Expected file ${gradleProperties.path}. ' + 'Please ensure that this file exists or that ${gradleProperties.dirname} can be read.' + ); } - gradleProperties.writeAsStringSync('android.enableR8=true\n', mode: FileMode.append); - } on FileSystemException { - throwToolExit( - 'The tool failed to add `android.enableR8=true` to ${gradleProperties.path}. ' - 'Please update the file manually and try this command again.' - ); - } -} - -/// Injects the Gradle wrapper files if any of these files don't exist in [directory]. -void injectGradleWrapperIfNeeded(Directory directory) { - copyDirectorySync( - cache.getArtifactDirectory('gradle_wrapper'), - directory, - shouldCopyFile: (File sourceFile, File destinationFile) { - // Don't override the existing files in the project. - return !destinationFile.existsSync(); - }, - onFileCopied: (File sourceFile, File destinationFile) { - final String modes = sourceFile.statSync().modeString(); - if (modes != null && modes.contains('x')) { - os.makeExecutable(destinationFile); + final String propertiesContent = gradleProperties.readAsStringSync(); + if (propertiesContent.contains('android.enableR8')) { + printTrace('gradle.properties already sets `android.enableR8`'); + return; + } + printTrace('set `android.enableR8=true` in gradle.properties'); + try { + if (propertiesContent.isNotEmpty && !propertiesContent.endsWith('\n')) { + // Add a new line if the file doesn't end with a new line. + gradleProperties.writeAsStringSync('\n', mode: FileMode.append); } - }, - ); - // Add the `gradle-wrapper.properties` file if it doesn't exist. - final File propertiesFile = directory.childFile( - fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); - if (!propertiesFile.existsSync()) { - final String gradleVersion = getGradleVersionForAndroidPlugin(directory); - propertiesFile.writeAsStringSync(''' + gradleProperties.writeAsStringSync('android.enableR8=true\n', mode: FileMode.append); + } on FileSystemException { + throwToolExit( + 'The tool failed to add `android.enableR8=true` to ${gradleProperties.path}. ' + 'Please update the file manually and try this command again.' + ); + } + } + + /// Injects the Gradle wrapper files if any of these files don't exist in [directory]. + void injectGradleWrapperIfNeeded(Directory directory) { + copyDirectorySync( + cache.getArtifactDirectory('gradle_wrapper'), + directory, + shouldCopyFile: (File sourceFile, File destinationFile) { + // Don't override the existing files in the project. + return !destinationFile.existsSync(); + }, + onFileCopied: (File sourceFile, File destinationFile) { + if (_hasExecutePermission(sourceFile)) { + _giveExecutePermissionIfNeeded(destinationFile); + } + }, + ); + // Add the `gradle-wrapper.properties` file if it doesn't exist. + final File propertiesFile = directory.childFile( + fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); + if (!propertiesFile.existsSync()) { + final String gradleVersion = getGradleVersionForAndroidPlugin(directory); + propertiesFile.writeAsStringSync(''' distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip ''', flush: true, - ); + ); + } } } - const String _defaultGradleVersion = '5.6.2'; final RegExp _androidPluginRegExp = RegExp('com\.android\.tools\.build\:gradle\:(\\d+\.\\d+\.\\d+\)'); @@ -150,6 +151,24 @@ return getGradleVersionFor(androidPluginVersion); } +const int _kExecPermissionMask = 0x49; // a+x + +/// Returns [true] if [executable] has execute permission. +bool _hasExecutePermission(File executable) { + final FileStat stat = executable.statSync(); + assert(stat.type != FileSystemEntityType.notFound); + printTrace('${executable.path} mode: ${stat.mode} ${stat.modeString()}.'); + return stat.mode & _kExecPermissionMask == _kExecPermissionMask; +} + +/// Gives execute permission to [executable] if it doesn't have it already. +void _giveExecutePermissionIfNeeded(File executable) { + if (!_hasExecutePermission(executable)) { + printTrace('Trying to give execute permission to ${executable.path}.'); + os.makeExecutable(executable); + } +} + /// Returns true if [targetVersion] is within the range [min] and [max] inclusive. bool _isWithinVersionRange( String targetVersion, {
diff --git a/packages/flutter_tools/lib/src/cache.dart b/packages/flutter_tools/lib/src/cache.dart index 3fb0dc1..db0dd78 100644 --- a/packages/flutter_tools/lib/src/cache.dart +++ b/packages/flutter_tools/lib/src/cache.dart
@@ -904,16 +904,13 @@ Future<void> update() async { final Directory tempDir = fs.systemTempDirectory.createTempSync('flutter_gradle_wrapper.'); - injectGradleWrapperIfNeeded(tempDir); + gradleUtils.injectGradleWrapperIfNeeded(tempDir); final Status status = logger.startProgress('Downloading Android Maven dependencies...', timeout: timeoutConfiguration.slowOperation); final File gradle = tempDir.childFile( platform.isWindows ? 'gradlew.bat' : 'gradlew', ); - assert(gradle.existsSync()); - os.makeExecutable(gradle); - try { final String gradleExecutable = gradle.absolute.path; final String flutterSdk = escapePath(Cache.flutterRoot);
diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 833bc6e..bc9dba0 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart
@@ -630,7 +630,7 @@ _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_common'), _editableHostAppDirectory); _overwriteFromTemplate(fs.path.join('module', 'android', 'host_app_editable'), _editableHostAppDirectory); _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), _editableHostAppDirectory); - gradle.injectGradleWrapperIfNeeded(_editableHostAppDirectory); + gradle.gradleUtils.injectGradleWrapperIfNeeded(_editableHostAppDirectory); gradle.writeLocalProperties(_editableHostAppDirectory.childFile('local.properties')); await injectPlugins(parent); } @@ -647,7 +647,7 @@ featureFlags.isAndroidEmbeddingV2Enabled ? 'library_new_embedding' : 'library', ), ephemeralDirectory); _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), ephemeralDirectory); - gradle.injectGradleWrapperIfNeeded(ephemeralDirectory); + gradle.gradleUtils.injectGradleWrapperIfNeeded(ephemeralDirectory); } void _overwriteFromTemplate(String path, Directory target) {
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart index 3f161bf..8317c06 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart
@@ -814,107 +814,6 @@ }); }); - group('migrateToR8', () { - MemoryFileSystem memoryFileSystem; - - setUp(() { - memoryFileSystem = MemoryFileSystem(); - }); - - testUsingContext('throws ToolExit if gradle.properties doesn\'t exist', () { - final Directory sampleAppAndroid = fs.directory('/sample-app/android'); - sampleAppAndroid.createSync(recursive: true); - - expect(() { - migrateToR8(sampleAppAndroid); - }, throwsToolExit(message: 'Expected file ${sampleAppAndroid.path}')); - - }, overrides: <Type, Generator>{ - FileSystem: () => memoryFileSystem, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('throws ToolExit if it cannot write gradle.properties', () { - final MockDirectory sampleAppAndroid = MockDirectory(); - final MockFile gradleProperties = MockFile(); - - when(gradleProperties.path).thenReturn('foo/gradle.properties'); - when(gradleProperties.existsSync()).thenReturn(true); - when(gradleProperties.readAsStringSync()).thenReturn(''); - when(gradleProperties.writeAsStringSync('android.enableR8=true\n', mode: FileMode.append)) - .thenThrow(const FileSystemException()); - - when(sampleAppAndroid.childFile('gradle.properties')) - .thenReturn(gradleProperties); - - expect(() { - migrateToR8(sampleAppAndroid); - }, - throwsToolExit(message: - 'The tool failed to add `android.enableR8=true` to foo/gradle.properties. ' - 'Please update the file manually and try this command again.')); - }); - - testUsingContext('does not update gradle.properties if it already uses R8', () { - final Directory sampleAppAndroid = fs.directory('/sample-app/android'); - sampleAppAndroid.createSync(recursive: true); - sampleAppAndroid.childFile('gradle.properties') - .writeAsStringSync('android.enableR8=true'); - - migrateToR8(sampleAppAndroid); - - expect(testLogger.traceText, - contains('gradle.properties already sets `android.enableR8`')); - expect(sampleAppAndroid.childFile('gradle.properties').readAsStringSync(), - equals('android.enableR8=true')); - }, overrides: <Type, Generator>{ - FileSystem: () => memoryFileSystem, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('sets android.enableR8=true', () { - final Directory sampleAppAndroid = fs.directory('/sample-app/android'); - sampleAppAndroid.createSync(recursive: true); - sampleAppAndroid.childFile('gradle.properties') - .writeAsStringSync('org.gradle.jvmargs=-Xmx1536M\n'); - - migrateToR8(sampleAppAndroid); - - expect(testLogger.traceText, contains('set `android.enableR8=true` in gradle.properties')); - expect( - sampleAppAndroid.childFile('gradle.properties').readAsStringSync(), - equals( - 'org.gradle.jvmargs=-Xmx1536M\n' - 'android.enableR8=true\n' - ), - ); - }, overrides: <Type, Generator>{ - FileSystem: () => memoryFileSystem, - ProcessManager: () => FakeProcessManager.any(), - }); - - testUsingContext('appends android.enableR8=true to the new line', () { - final Directory sampleAppAndroid = fs.directory('/sample-app/android'); - sampleAppAndroid.createSync(recursive: true); - sampleAppAndroid.childFile('gradle.properties') - .writeAsStringSync('org.gradle.jvmargs=-Xmx1536M'); - - migrateToR8(sampleAppAndroid); - - expect(testLogger.traceText, contains('set `android.enableR8=true` in gradle.properties')); - expect( - sampleAppAndroid.childFile('gradle.properties').readAsStringSync(), - equals( - 'org.gradle.jvmargs=-Xmx1536M\n' - 'android.enableR8=true\n' - ), - ); - }, overrides: <Type, Generator>{ - FileSystem: () => memoryFileSystem, - ProcessManager: () => FakeProcessManager.any() - }); - }); - group('isAppUsingAndroidX', () { FileSystem fs;
diff --git a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart index e53fe29..b5cdc97 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart
@@ -6,7 +6,10 @@ import 'package:flutter_tools/src/android/gradle_utils.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/os.dart'; +import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/cache.dart'; +import 'package:flutter_tools/src/project.dart'; +import 'package:mockito/mockito.dart'; import 'package:process/process.dart'; import '../../src/common.dart'; @@ -38,11 +41,11 @@ .writeAsStringSync('irrelevant'); }); - testUsingContext('Inject the wrapper when all files are missing', () { + testUsingContext('injects the wrapper when all files are missing', () { final Directory sampleAppAndroid = fs.directory('/sample-app/android'); sampleAppAndroid.createSync(recursive: true); - injectGradleWrapperIfNeeded(sampleAppAndroid); + gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid); expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue); @@ -74,14 +77,14 @@ ProcessManager: () => FakeProcessManager.any(), }); - testUsingContext('Inject the wrapper when some files are missing', () { + testUsingContext('injects the wrapper when some files are missing', () { final Directory sampleAppAndroid = fs.directory('/sample-app/android'); sampleAppAndroid.createSync(recursive: true); // There's an existing gradlew sampleAppAndroid.childFile('gradlew').writeAsStringSync('existing gradlew'); - injectGradleWrapperIfNeeded(sampleAppAndroid); + gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid); expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue); expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(), @@ -114,24 +117,211 @@ FileSystem: () => memoryFileSystem, ProcessManager: () => FakeProcessManager.any(), }); + }); - testUsingContext('Gives executable permission to gradle', () { + + group('migrateToR8', () { + MemoryFileSystem memoryFileSystem; + + setUp(() { + memoryFileSystem = MemoryFileSystem(); + }); + + testUsingContext('throws ToolExit if gradle.properties doesn\'t exist', () { final Directory sampleAppAndroid = fs.directory('/sample-app/android'); sampleAppAndroid.createSync(recursive: true); - // Make gradlew in the wrapper executable. - os.makeExecutable(gradleWrapperDirectory.childFile('gradlew')); + expect(() { + gradleUtils.migrateToR8(sampleAppAndroid); + }, throwsToolExit(message: 'Expected file ${sampleAppAndroid.path}')); - injectGradleWrapperIfNeeded(sampleAppAndroid); - - final File gradlew = sampleAppAndroid.childFile('gradlew'); - expect(gradlew.existsSync(), isTrue); - expect(gradlew.statSync().modeString().contains('x'), isTrue); }, overrides: <Type, Generator>{ - Cache: () => Cache(rootOverride: tempDir), FileSystem: () => memoryFileSystem, ProcessManager: () => FakeProcessManager.any(), - OperatingSystemUtils: () => OperatingSystemUtils(), + }); + + testUsingContext('throws ToolExit if it cannot write gradle.properties', () { + final MockDirectory sampleAppAndroid = MockDirectory(); + final MockFile gradleProperties = MockFile(); + + when(gradleProperties.path).thenReturn('foo/gradle.properties'); + when(gradleProperties.existsSync()).thenReturn(true); + when(gradleProperties.readAsStringSync()).thenReturn(''); + when(gradleProperties.writeAsStringSync('android.enableR8=true\n', mode: FileMode.append)) + .thenThrow(const FileSystemException()); + + when(sampleAppAndroid.childFile('gradle.properties')) + .thenReturn(gradleProperties); + + expect(() { + gradleUtils.migrateToR8(sampleAppAndroid); + }, + throwsToolExit(message: + 'The tool failed to add `android.enableR8=true` to foo/gradle.properties. ' + 'Please update the file manually and try this command again.')); + }); + + testUsingContext('does not update gradle.properties if it already uses R8', () { + final Directory sampleAppAndroid = fs.directory('/sample-app/android'); + sampleAppAndroid.createSync(recursive: true); + sampleAppAndroid.childFile('gradle.properties') + .writeAsStringSync('android.enableR8=true'); + + gradleUtils.migrateToR8(sampleAppAndroid); + + expect(testLogger.traceText, + contains('gradle.properties already sets `android.enableR8`')); + expect(sampleAppAndroid.childFile('gradle.properties').readAsStringSync(), + equals('android.enableR8=true')); + }, overrides: <Type, Generator>{ + FileSystem: () => memoryFileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('sets android.enableR8=true', () { + final Directory sampleAppAndroid = fs.directory('/sample-app/android'); + sampleAppAndroid.createSync(recursive: true); + sampleAppAndroid.childFile('gradle.properties') + .writeAsStringSync('org.gradle.jvmargs=-Xmx1536M\n'); + + gradleUtils.migrateToR8(sampleAppAndroid); + + expect(testLogger.traceText, contains('set `android.enableR8=true` in gradle.properties')); + expect( + sampleAppAndroid.childFile('gradle.properties').readAsStringSync(), + equals( + 'org.gradle.jvmargs=-Xmx1536M\n' + 'android.enableR8=true\n' + ), + ); + }, overrides: <Type, Generator>{ + FileSystem: () => memoryFileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('appends android.enableR8=true to the new line', () { + final Directory sampleAppAndroid = fs.directory('/sample-app/android'); + sampleAppAndroid.createSync(recursive: true); + sampleAppAndroid.childFile('gradle.properties') + .writeAsStringSync('org.gradle.jvmargs=-Xmx1536M'); + + gradleUtils.migrateToR8(sampleAppAndroid); + + expect(testLogger.traceText, contains('set `android.enableR8=true` in gradle.properties')); + expect( + sampleAppAndroid.childFile('gradle.properties').readAsStringSync(), + equals( + 'org.gradle.jvmargs=-Xmx1536M\n' + 'android.enableR8=true\n' + ), + ); + }, overrides: <Type, Generator>{ + FileSystem: () => memoryFileSystem, + ProcessManager: () => FakeProcessManager.any() }); }); -} \ No newline at end of file + + group('GradleUtils.getExecutable', () { + final String gradlewFilename = platform.isWindows ? 'gradlew.bat' : 'gradlew'; + + MemoryFileSystem memoryFileSystem; + OperatingSystemUtils operatingSystemUtils; + MockGradleUtils gradleUtils; + + setUp(() { + memoryFileSystem = MemoryFileSystem(); + operatingSystemUtils = MockOperatingSystemUtils(); + gradleUtils = MockGradleUtils(); + }); + + testUsingContext('returns the gradlew path', () { + final Directory androidDirectory = fs.directory('/android')..createSync(); + androidDirectory.childFile('gradlew')..createSync(); + androidDirectory.childFile('gradlew.bat')..createSync(); + androidDirectory.childFile('gradle.properties').createSync(); + + when(gradleUtils.injectGradleWrapperIfNeeded(any)).thenReturn(null); + when(gradleUtils.migrateToR8(any)).thenReturn(null); + + expect( + GradleUtils().getExecutable(FlutterProject.current()), + androidDirectory.childFile(gradlewFilename).path, + ); + }, overrides: <Type, Generator>{ + FileSystem: () => memoryFileSystem, + ProcessManager: () => FakeProcessManager.any(), + OperatingSystemUtils: () => operatingSystemUtils, + GradleUtils: () => gradleUtils, + }); + + testUsingContext('gives execute permission to gradle', () { + final FlutterProject flutterProject = MockFlutterProject(); + final AndroidProject androidProject = MockAndroidProject(); + when(flutterProject.android).thenReturn(androidProject); + + final FileStat gradleStat = MockFileStat(); + when(gradleStat.mode).thenReturn(444); + + final File gradlew = MockFile(); + when(gradlew.path).thenReturn('gradlew'); + when(gradlew.absolute).thenReturn(gradlew); + when(gradlew.statSync()).thenReturn(gradleStat); + when(gradlew.existsSync()).thenReturn(true); + + final Directory androidDirectory = MockDirectory(); + when(androidDirectory.childFile(gradlewFilename)).thenReturn(gradlew); + when(androidProject.hostAppGradleRoot).thenReturn(androidDirectory); + + when(gradleUtils.injectGradleWrapperIfNeeded(any)).thenReturn(null); + when(gradleUtils.migrateToR8(any)).thenReturn(null); + + GradleUtils().getExecutable(flutterProject); + + verify(operatingSystemUtils.makeExecutable(gradlew)).called(1); + }, overrides: <Type, Generator>{ + FileSystem: () => memoryFileSystem, + ProcessManager: () => FakeProcessManager.any(), + OperatingSystemUtils: () => operatingSystemUtils, + GradleUtils: () => gradleUtils, + }); + + testUsingContext('doesn\'t give execute permission to gradle if not needed', () { + final FlutterProject flutterProject = MockFlutterProject(); + final AndroidProject androidProject = MockAndroidProject(); + when(flutterProject.android).thenReturn(androidProject); + + final FileStat gradleStat = MockFileStat(); + when(gradleStat.mode).thenReturn(0x49 /* a+x */); + + final File gradlew = MockFile(); + when(gradlew.path).thenReturn('gradlew'); + when(gradlew.absolute).thenReturn(gradlew); + when(gradlew.statSync()).thenReturn(gradleStat); + when(gradlew.existsSync()).thenReturn(true); + + final Directory androidDirectory = MockDirectory(); + when(androidDirectory.childFile(gradlewFilename)).thenReturn(gradlew); + when(androidProject.hostAppGradleRoot).thenReturn(androidDirectory); + + when(gradleUtils.injectGradleWrapperIfNeeded(any)).thenReturn(null); + when(gradleUtils.migrateToR8(any)).thenReturn(null); + + GradleUtils().getExecutable(flutterProject); + + verifyNever(operatingSystemUtils.makeExecutable(gradlew)); + }, overrides: <Type, Generator>{ + FileSystem: () => memoryFileSystem, + ProcessManager: () => FakeProcessManager.any(), + OperatingSystemUtils: () => operatingSystemUtils, + GradleUtils: () => gradleUtils, + }); + }); +} + +class MockAndroidProject extends Mock implements AndroidProject {} +class MockDirectory extends Mock implements Directory {} +class MockFile extends Mock implements File {} +class MockFileStat extends Mock implements FileStat {} +class MockFlutterProject extends Mock implements FlutterProject {} +class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils {} +class MockGradleUtils extends Mock implements GradleUtils {}