From 4a1c62c28b3da29e3b72d81b42792643717b2200 Mon Sep 17 00:00:00 2001 From: Emmanuel Garcia Date: Wed, 28 Aug 2019 14:52:08 -0700 Subject: [PATCH] Add missing files in the Gradle wrapper directory (#39145) --- .../flutter_tools/lib/src/android/gradle.dart | 41 +++-- .../lib/src/base/file_system.dart | 24 ++- .../lib/src/commands/create.dart | 2 +- packages/flutter_tools/lib/src/project.dart | 4 +- .../general.shard/android/gradle_test.dart | 144 ++++++++++++++++++ .../application_package_test.dart | 12 ++ .../general.shard/base/file_system_test.dart | 23 +++ 7 files changed, 227 insertions(+), 23 deletions(-) diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 9f5953dfeb6..2b14a1afbc1 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -268,13 +268,10 @@ String _locateGradlewExecutable(Directory directory) { final File gradle = directory.childFile( platform.isWindows ? 'gradlew.bat' : 'gradlew', ); - if (gradle.existsSync()) { - os.makeExecutable(gradle); return gradle.absolute.path; - } else { - return null; } + return null; } Future _ensureGradle(FlutterProject project) async { @@ -286,12 +283,12 @@ Future _ensureGradle(FlutterProject project) async { // of validating the Gradle executable. This may take several seconds. Future _initializeGradle(FlutterProject project) async { final Directory android = project.android.hostAppGradleRoot; - final Status status = logger.startProgress('Initializing gradle...', timeout: timeoutConfiguration.slowOperation); - String gradle = _locateGradlewExecutable(android); - if (gradle == null) { - injectGradleWrapper(android); - gradle = _locateGradlewExecutable(android); - } + final Status status = logger.startProgress('Initializing gradle...', + timeout: timeoutConfiguration.slowOperation); + + injectGradleWrapperIfNeeded(android); + + final String gradle = _locateGradlewExecutable(android); if (gradle == null) throwToolExit('Unable to locate gradlew script'); printTrace('Using gradle from $gradle.'); @@ -302,11 +299,25 @@ Future _initializeGradle(FlutterProject project) async { return gradle; } -/// Injects the Gradle wrapper into the specified directory. -void injectGradleWrapper(Directory directory) { - copyDirectorySync(cache.getArtifactDirectory('gradle_wrapper'), directory); - _locateGradlewExecutable(directory); - final File propertiesFile = directory.childFile(fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); +/// 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); + } + }, + ); + // 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(''' diff --git a/packages/flutter_tools/lib/src/base/file_system.dart b/packages/flutter_tools/lib/src/base/file_system.dart index 1048e32bb6d..3f7dadb58e2 100644 --- a/packages/flutter_tools/lib/src/base/file_system.dart +++ b/packages/flutter_tools/lib/src/base/file_system.dart @@ -64,11 +64,18 @@ void ensureDirectoryExists(String filePath) { } } -/// Recursively copies `srcDir` to `destDir`, invoking [onFileCopied] if -/// specified for each source/destination file pair. +/// Creates `destDir` if needed, then recursively copies `srcDir` to `destDir`, +/// invoking [onFileCopied], if specified, for each source/destination file pair. /// -/// Creates `destDir` if needed. -void copyDirectorySync(Directory srcDir, Directory destDir, [ void onFileCopied(File srcFile, File destFile) ]) { +/// Skips files if [shouldCopyFile] returns `false`. +void copyDirectorySync( + Directory srcDir, + Directory destDir, + { + bool shouldCopyFile(File srcFile, File destFile), + void onFileCopied(File srcFile, File destFile), + } +) { if (!srcDir.existsSync()) throw Exception('Source directory "${srcDir.path}" does not exist, nothing to copy'); @@ -79,11 +86,18 @@ void copyDirectorySync(Directory srcDir, Directory destDir, [ void onFileCopied( final String newPath = destDir.fileSystem.path.join(destDir.path, entity.basename); if (entity is File) { final File newFile = destDir.fileSystem.file(newPath); + if (shouldCopyFile != null && !shouldCopyFile(entity, newFile)) { + continue; + } newFile.writeAsBytesSync(entity.readAsBytesSync()); onFileCopied?.call(entity, newFile); } else if (entity is Directory) { copyDirectorySync( - entity, destDir.fileSystem.directory(newPath)); + entity, + destDir.fileSystem.directory(newPath), + shouldCopyFile: shouldCopyFile, + onFileCopied: onFileCopied, + ); } else { throw Exception('${entity.path} is neither File nor Directory'); } diff --git a/packages/flutter_tools/lib/src/commands/create.dart b/packages/flutter_tools/lib/src/commands/create.dart index c29458ab2fa..3dee11ac18e 100644 --- a/packages/flutter_tools/lib/src/commands/create.dart +++ b/packages/flutter_tools/lib/src/commands/create.dart @@ -628,7 +628,7 @@ To edit platform code in an IDE see https://flutter.dev/developing-packages/#edi copyDirectorySync( cache.getArtifactDirectory('gradle_wrapper'), project.android.hostAppGradleRoot, - (File sourceFile, File destinationFile) { + onFileCopied: (File sourceFile, File destinationFile) { filesCreated++; final String modes = sourceFile.statSync().modeString(); if (modes != null && modes.contains('x')) { diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index dc2552c9a8f..e33f679fa7f 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -580,7 +580,7 @@ class AndroidProject { _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.injectGradleWrapper(_editableHostAppDirectory); + gradle.injectGradleWrapperIfNeeded(_editableHostAppDirectory); gradle.writeLocalProperties(_editableHostAppDirectory.childFile('local.properties')); await injectPlugins(parent); } @@ -593,7 +593,7 @@ class AndroidProject { _deleteIfExistsSync(ephemeralDirectory); _overwriteFromTemplate(fs.path.join('module', 'android', 'library'), ephemeralDirectory); _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), ephemeralDirectory); - gradle.injectGradleWrapper(ephemeralDirectory); + gradle.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 b33bbc0e67b..313055faa80 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/ios/xcodeproj.dart'; @@ -848,6 +849,125 @@ flutter: }); }); + group('injectGradleWrapperIfNeeded', () { + MemoryFileSystem memoryFileSystem; + Directory tempDir; + Directory gradleWrapperDirectory; + + setUp(() { + memoryFileSystem = MemoryFileSystem(); + tempDir = memoryFileSystem.systemTempDirectory.createTempSync('artifacts_test.'); + gradleWrapperDirectory = memoryFileSystem.directory( + memoryFileSystem.path.join(tempDir.path, 'bin', 'cache', 'artifacts', 'gradle_wrapper')); + gradleWrapperDirectory.createSync(recursive: true); + gradleWrapperDirectory + .childFile('gradlew') + .writeAsStringSync('irrelevant'); + gradleWrapperDirectory + .childDirectory('gradle') + .childDirectory('wrapper') + .createSync(recursive: true); + gradleWrapperDirectory + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .writeAsStringSync('irrelevant'); + }); + + testUsingContext('Inject the wrapper when all files are missing', () { + final Directory sampleAppAndroid = fs.directory('/sample-app/android'); + sampleAppAndroid.createSync(recursive: true); + + injectGradleWrapperIfNeeded(sampleAppAndroid); + + expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue); + + expect(sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .existsSync(), isTrue); + + expect(sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .existsSync(), isTrue); + + expect(sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .readAsStringSync(), + 'distributionBase=GRADLE_USER_HOME\n' + 'distributionPath=wrapper/dists\n' + 'zipStoreBase=GRADLE_USER_HOME\n' + 'zipStorePath=wrapper/dists\n' + 'distributionUrl=https\\://services.gradle.org/distributions/gradle-4.10.2-all.zip\n'); + }, overrides: { + Cache: () => Cache(rootOverride: tempDir), + FileSystem: () => memoryFileSystem, + }); + + testUsingContext('Inject 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); + + expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue); + expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(), + equals('existing gradlew')); + + expect(sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .existsSync(), isTrue); + + expect(sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .existsSync(), isTrue); + + expect(sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .readAsStringSync(), + 'distributionBase=GRADLE_USER_HOME\n' + 'distributionPath=wrapper/dists\n' + 'zipStoreBase=GRADLE_USER_HOME\n' + 'zipStorePath=wrapper/dists\n' + 'distributionUrl=https\\://services.gradle.org/distributions/gradle-4.10.2-all.zip\n'); + }, overrides: { + Cache: () => Cache(rootOverride: tempDir), + FileSystem: () => memoryFileSystem, + }); + + testUsingContext('Gives executable permission to gradle', () { + final Directory sampleAppAndroid = fs.directory('/sample-app/android'); + sampleAppAndroid.createSync(recursive: true); + + // Make gradlew in the wrapper executable. + os.makeExecutable(gradleWrapperDirectory.childFile('gradlew')); + + injectGradleWrapperIfNeeded(sampleAppAndroid); + + final File gradlew = sampleAppAndroid.childFile('gradlew'); + expect(gradlew.existsSync(), isTrue); + expect(gradlew.statSync().modeString().contains('x'), isTrue); + }, overrides: { + Cache: () => Cache(rootOverride: tempDir), + FileSystem: () => memoryFileSystem, + OperatingSystemUtils: () => OperatingSystemUtils(), + }); + }); + group('gradle build', () { MockAndroidSdk mockAndroidSdk; MockAndroidStudio mockAndroidStudio; @@ -855,6 +975,7 @@ flutter: MockProcessManager mockProcessManager; FakePlatform android; FileSystem fs; + Cache cache; setUp(() { fs = MemoryFileSystem(); @@ -863,6 +984,28 @@ flutter: mockArtifacts = MockLocalEngineArtifacts(); mockProcessManager = MockProcessManager(); android = fakePlatform('android'); + + final Directory tempDir = fs.systemTempDirectory.createTempSync('artifacts_test.'); + cache = Cache(rootOverride: tempDir); + + final Directory gradleWrapperDirectory = tempDir + .childDirectory('bin') + .childDirectory('cache') + .childDirectory('artifacts') + .childDirectory('gradle_wrapper'); + gradleWrapperDirectory.createSync(recursive: true); + gradleWrapperDirectory + .childFile('gradlew') + .writeAsStringSync('irrelevant'); + gradleWrapperDirectory + .childDirectory('gradle') + .childDirectory('wrapper') + .createSync(recursive: true); + gradleWrapperDirectory + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .writeAsStringSync('irrelevant'); }); testUsingContext('build aar uses selected local engine', () async { @@ -928,6 +1071,7 @@ flutter: AndroidSdk: () => mockAndroidSdk, AndroidStudio: () => mockAndroidStudio, Artifacts: () => mockArtifacts, + Cache: () => cache, ProcessManager: () => mockProcessManager, Platform: () => android, FileSystem: () => fs, diff --git a/packages/flutter_tools/test/general.shard/application_package_test.dart b/packages/flutter_tools/test/general.shard/application_package_test.dart index 31ce93f3e59..c2fbedbbdf0 100644 --- a/packages/flutter_tools/test/general.shard/application_package_test.dart +++ b/packages/flutter_tools/test/general.shard/application_package_test.dart @@ -39,17 +39,20 @@ void main() { AndroidSdk sdk; ProcessManager mockProcessManager; MemoryFileSystem fs; + Cache mockCache; File gradle; final Map overrides = { AndroidSdk: () => sdk, ProcessManager: () => mockProcessManager, FileSystem: () => fs, + Cache: () => mockCache, }; setUp(() async { sdk = MockitoAndroidSdk(); mockProcessManager = MockitoProcessManager(); fs = MemoryFileSystem(); + mockCache = MockCache(); Cache.flutterRoot = '../..'; when(sdk.licensesAvailable).thenReturn(true); when(mockProcessManager.canRun(any)).thenReturn(true); @@ -100,6 +103,14 @@ void main() { platform.isWindows ? 'gradlew.bat' : 'gradlew', )..createSync(recursive: true); + final Directory gradleWrapperDir = fs.systemTempDirectory.createTempSync('gradle_wrapper.'); + when(mockCache.getArtifactDirectory('gradle_wrapper')).thenReturn(gradleWrapperDir); + + fs.directory(gradleWrapperDir.childDirectory('gradle').childDirectory('wrapper')) + .createSync(recursive: true); + fs.file(fs.path.join(gradleWrapperDir.path, 'gradlew')).writeAsStringSync('irrelevant'); + fs.file(fs.path.join(gradleWrapperDir.path, 'gradlew.bat')).writeAsStringSync('irrelevant'); + await ApplicationPackageFactory.instance.getPackageForPlatform( TargetPlatform.android_arm, applicationBinary: fs.file('app.apk'), @@ -606,4 +617,5 @@ const String plistData = ''' {"CFBundleIdentifier": "fooBundleId"} '''; +class MockCache extends Mock implements Cache {} class MockOperatingSystemUtils extends Mock implements OperatingSystemUtils { } diff --git a/packages/flutter_tools/test/general.shard/base/file_system_test.dart b/packages/flutter_tools/test/general.shard/base/file_system_test.dart index 09126acb6da..e0a4155a87d 100644 --- a/packages/flutter_tools/test/general.shard/base/file_system_test.dart +++ b/packages/flutter_tools/test/general.shard/base/file_system_test.dart @@ -58,6 +58,29 @@ void main() { // There's still 3 things in the original directory as there were initially. expect(sourceMemoryFs.directory(sourcePath).listSync().length, 3); }); + + testUsingContext('Skip files if shouldCopyFile returns false', () { + final Directory origin = fs.directory('/origin'); + origin.createSync(); + fs.file(fs.path.join('origin', 'a.txt')).writeAsStringSync('irrelevant'); + fs.directory('/origin/nested').createSync(); + fs.file(fs.path.join('origin', 'nested', 'a.txt')).writeAsStringSync('irrelevant'); + fs.file(fs.path.join('origin', 'nested', 'b.txt')).writeAsStringSync('irrelevant'); + + final Directory destination = fs.directory('/destination'); + copyDirectorySync(origin, destination, shouldCopyFile: (File origin, File dest) { + return origin.basename == 'b.txt'; + }); + + expect(destination.existsSync(), isTrue); + expect(destination.childDirectory('nested').existsSync(), isTrue); + expect(destination.childDirectory('nested').childFile('b.txt').existsSync(), isTrue); + + expect(destination.childFile('a.txt').existsSync(), isFalse); + expect(destination.childDirectory('nested').childFile('a.txt').existsSync(), isFalse); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + }); }); group('canonicalizePath', () {