add version to pubspec.yaml (#16857)

Uses the `version` property from the `pubspec.yaml` file to set the corresponding fields in the `local.properties` file respectively in the `Generated.xcconfig` file.

The `--build-name` and `--build-number` options have changed. Now they trump the `version` property from the `pubspec.yaml` file.

If the `version` property is not set and the  `--build-name` and `--build-number` options are not provided, the build command will not change the `local.properties` / `Generated.xcconfig` file.
diff --git a/packages/flutter_tools/test/android/gradle_test.dart b/packages/flutter_tools/test/android/gradle_test.dart
index 5da37d5..c03af96 100644
--- a/packages/flutter_tools/test/android/gradle_test.dart
+++ b/packages/flutter_tools/test/android/gradle_test.dart
@@ -2,16 +2,20 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
+
 import 'package:flutter_tools/src/android/gradle.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/cache.dart';
 import 'package:test/test.dart';
 
 import '../src/common.dart';
+import '../src/context.dart';
 
 void main() {
   group('gradle build', () {
-    test('do not crash if there is no Android SDK', () {
+    test('do not crash if there is no Android SDK', () async {
       Exception shouldBeToolExit;
       try {
         // We'd like to always set androidSdk to null and test updateLocalProperties. But that's
@@ -21,7 +25,7 @@
         // This test is written to fail if our bots get Android SDKs in the future: shouldBeToolExit
         // will be null and our expectation would fail. That would remind us to make these tests
         // hermetic before adding Android SDKs to the bots.
-        updateLocalProperties();
+        await updateLocalProperties();
       } on Exception catch (e) {
         shouldBeToolExit = e;
       }
@@ -126,4 +130,181 @@
       expect(project.assembleTaskFor(const BuildInfo(BuildMode.release, 'unknown')), isNull);
     });
   });
+
+  group('Gradle local.properties', () {
+    Directory temp;
+
+    setUp(() {
+      Cache.disableLocking();
+      temp = fs.systemTempDirectory.createTempSync('flutter_tools');
+    });
+
+    tearDown(() {
+      temp.deleteSync(recursive: true);
+    });
+
+    Future<String> createMinimalProject(String manifest) async {
+      final Directory directory = temp.childDirectory('android_project');
+      final File manifestFile = directory.childFile('pubspec.yaml');
+      manifestFile.createSync(recursive: true);
+      manifestFile.writeAsStringSync(manifest);
+
+      return directory.path;
+    }
+
+    String propertyFor(String key, File file) {
+      return file
+          .readAsLinesSync()
+          .where((String line) => line.startsWith('$key='))
+          .map((String line) => line.split('=')[1])
+          .first;
+    }
+
+    Future<void> checkBuildVersion({
+      String manifest,
+      BuildInfo buildInfo,
+      String expectedBuildName,
+      String expectedBuildNumber,
+    }) async {
+      final String projectPath = await createMinimalProject(manifest);
+
+      try {
+        await updateLocalProperties(projectPath: projectPath, buildInfo: buildInfo);
+
+        final String propertiesPath = fs.path.join(projectPath, 'android', 'local.properties');
+        final File localPropertiesFile = fs.file(propertiesPath);
+
+        expect(propertyFor('flutter.versionName', localPropertiesFile), expectedBuildName);
+        expect(propertyFor('flutter.versionCode', localPropertiesFile), expectedBuildNumber);
+      } on Exception {
+        // Android SDK not found, skip test
+      }
+    }
+
+    testUsingContext('extract build name and number from pubspec.yaml', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: '1',
+      );
+    });
+
+    testUsingContext('extract build name from pubspec.yaml', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: null,
+      );
+    });
+
+    testUsingContext('allow build info to override build name', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2');
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '1',
+      );
+    });
+
+    testUsingContext('allow build info to override build number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildNumber: 3);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingContext('allow build info to override build name and number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingContext('allow build info to override build name and set number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingContext('allow build info to set build name and number', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+  });
 }
diff --git a/packages/flutter_tools/test/flutter_manifest_test.dart b/packages/flutter_tools/test/flutter_manifest_test.dart
index 75be31c..91c42cc 100644
--- a/packages/flutter_tools/test/flutter_manifest_test.dart
+++ b/packages/flutter_tools/test/flutter_manifest_test.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
+
 import 'package:file/file.dart';
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
@@ -359,6 +361,118 @@
       final FlutterManifest flutterManifest = await FlutterManifest.createFromString(manifest);
       expect(flutterManifest.isEmpty, false);
     });
+
+    Future<void> checkManifestVersion({
+      String manifest,
+      String expectedAppVersion,
+      String expectedBuildName,
+      int expectedBuildNumber,
+    }) async {
+      final FlutterManifest flutterManifest = await FlutterManifest.createFromString(manifest);
+      expect(flutterManifest.appVersion, expectedAppVersion);
+      expect(flutterManifest.buildName, expectedBuildName);
+      expect(flutterManifest.buildNumber, expectedBuildNumber);
+    }
+
+    test('parses major.minor.patch+build version clause', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+2
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: '1.0.0+2',
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: 2,
+      );
+    });
+
+    test('parses major.minor+build version clause', () async {
+      const String manifest = '''
+name: test
+version: 1.0+2
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: '1.0+2',
+        expectedBuildName: '1.0',
+        expectedBuildNumber: 2,
+      );
+    });
+
+    test('parses major+build version clause', () async {
+      const String manifest = '''
+name: test
+version: 1+2
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: '1+2',
+        expectedBuildName: '1',
+        expectedBuildNumber: 2,
+      );
+    });
+
+    test('parses major version clause', () async {
+      const String manifest = '''
+name: test
+version: 1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: '1',
+        expectedBuildName: '1',
+        expectedBuildNumber: null,
+      );
+    });
+
+    test('parses empty version clause', () async {
+      const String manifest = '''
+name: test
+version:
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: null,
+        expectedBuildName: null,
+        expectedBuildNumber: null,
+      );
+    });
+    test('parses no version clause', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      await checkManifestVersion(
+        manifest: manifest,
+        expectedAppVersion: null,
+        expectedBuildName: null,
+        expectedBuildNumber: null,
+      );
+    });
   });
 
   group('FlutterManifest with MemoryFileSystem', () {
@@ -371,8 +485,7 @@
 flutter:
 ''';
 
-      final FlutterManifest flutterManifest = await FlutterManifest
-          .createFromString(manifest);
+      final FlutterManifest flutterManifest = await FlutterManifest.createFromString(manifest);
       expect(flutterManifest.isEmpty, false);
     }
 
diff --git a/packages/flutter_tools/test/ios/xcodeproj_test.dart b/packages/flutter_tools/test/ios/xcodeproj_test.dart
index dd75f93..95f74ba 100644
--- a/packages/flutter_tools/test/ios/xcodeproj_test.dart
+++ b/packages/flutter_tools/test/ios/xcodeproj_test.dart
@@ -2,11 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:async';
+
 import 'package:file/memory.dart';
 import 'package:flutter_tools/src/artifacts.dart';
 import 'package:flutter_tools/src/base/file_system.dart';
 import 'package:flutter_tools/src/base/io.dart';
 import 'package:flutter_tools/src/build_info.dart';
+import 'package:flutter_tools/src/bundle.dart' as bundle;
+import 'package:flutter_tools/src/cache.dart';
 import 'package:flutter_tools/src/ios/xcodeproj.dart';
 import 'package:mockito/mockito.dart';
 import 'package:platform/platform.dart';
@@ -277,14 +281,14 @@
       });
     }
 
-    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () {
+    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
       when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, any)).thenReturn('engine');
       when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile_arm'));
       const BuildInfo buildInfo = const BuildInfo(BuildMode.debug, null,
         previewDart2: true,
         targetPlatform: TargetPlatform.ios,
       );
-      updateGeneratedXcodeProperties(
+      await updateGeneratedXcodeProperties(
         projectPath: 'path/to/project',
         buildInfo: buildInfo,
         previewDart2: true,
@@ -297,14 +301,14 @@
       expect(contents.contains('ARCHS=armv7'), isTrue);
     });
 
-    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () {
+    testUsingOsxContext('sets ARCHS=armv7 when armv7 local engine is set', () async {
       when(mockArtifacts.getArtifactPath(Artifact.flutterFramework, TargetPlatform.ios, any)).thenReturn('engine');
       when(mockArtifacts.engineOutPath).thenReturn(fs.path.join('out', 'ios_profile'));
       const BuildInfo buildInfo = const BuildInfo(BuildMode.debug, null,
         previewDart2: true,
         targetPlatform: TargetPlatform.ios,
       );
-      updateGeneratedXcodeProperties(
+      await updateGeneratedXcodeProperties(
         projectPath: 'path/to/project',
         buildInfo: buildInfo,
         previewDart2: true,
@@ -317,6 +321,185 @@
       expect(contents.contains('ARCHS=arm64'), isTrue);
     });
   });
+
+  group('Xcode Generated.xcconfig', () {
+    Directory temp;
+
+    setUp(() {
+      Cache.disableLocking();
+      temp = fs.systemTempDirectory.createTempSync('flutter_tools');
+    });
+
+    tearDown(() {
+      temp.deleteSync(recursive: true);
+    });
+
+    Future<String> createMinimalProject(String manifest) async {
+      final Directory directory = temp.childDirectory('ios_project');
+      final File manifestFile = directory.childFile('pubspec.yaml');
+      manifestFile.createSync(recursive: true);
+      manifestFile.writeAsStringSync(manifest);
+
+      return directory.path;
+    }
+
+    String propertyFor(String key, File file) {
+      final List<String> properties = file
+          .readAsLinesSync()
+          .where((String line) => line.startsWith('$key='))
+          .map((String line) => line.split('=')[1])
+          .toList();
+      return properties.isEmpty ? null : properties.first;
+    }
+
+    Future<void> checkBuildVersion({
+      String manifest,
+      BuildInfo buildInfo,
+      String expectedBuildName,
+      String expectedBuildNumber,
+    }) async {
+      final String projectPath = await createMinimalProject(manifest);
+
+      await updateGeneratedXcodeProperties(
+        projectPath: projectPath,
+        buildInfo: buildInfo,
+        targetOverride: bundle.defaultMainPath,
+        previewDart2: false,
+      );
+
+      final String propertiesPath = fs.path.join(projectPath, 'ios', 'Flutter', 'Generated.xcconfig');
+      final File localPropertiesFile = fs.file(propertiesPath);
+
+      expect(propertyFor('FLUTTER_BUILD_NAME', localPropertiesFile), expectedBuildName);
+      expect(propertyFor('FLUTTER_BUILD_NUMBER', localPropertiesFile), expectedBuildNumber);
+    }
+
+    testUsingContext('extract build name and number from pubspec.yaml', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: '1',
+      );
+    });
+
+    testUsingContext('extract build name from pubspec.yaml', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: null,
+      );
+    });
+
+    testUsingContext('allow build info to override build name', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2');
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '1',
+      );
+    });
+
+    testUsingContext('allow build info to override build number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildNumber: 3);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.0',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingContext('allow build info to override build name and number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0+1
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingContext('allow build info to override build name and set number', () async {
+      const String manifest = '''
+name: test
+version: 1.0.0
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+
+    testUsingContext('allow build info to set build name and number', () async {
+      const String manifest = '''
+name: test
+dependencies:
+  flutter:
+    sdk: flutter
+flutter:
+''';
+      const BuildInfo buildInfo = const BuildInfo(BuildMode.release, null, buildName: '1.0.2', buildNumber: 3);
+      await checkBuildVersion(
+        manifest: manifest,
+        buildInfo: buildInfo,
+        expectedBuildName: '1.0.2',
+        expectedBuildNumber: '3',
+      );
+    });
+  });
 }
 
 Platform fakePlatform(String name) {
diff --git a/packages/flutter_tools/test/project_test.dart b/packages/flutter_tools/test/project_test.dart
index 632018d..8955187 100644
--- a/packages/flutter_tools/test/project_test.dart
+++ b/packages/flutter_tools/test/project_test.dart
@@ -20,23 +20,23 @@
     group('ensure ready for platform-specific tooling', () {
       testInMemory('does nothing, if project is not created', () async {
         final FlutterProject project = someProject();
-        project.ensureReadyForPlatformSpecificTooling();
+        await project.ensureReadyForPlatformSpecificTooling();
         expect(project.directory.existsSync(), isFalse);
       });
       testInMemory('does nothing in plugin or package root project', () async {
         final FlutterProject project = aPluginProject();
-        project.ensureReadyForPlatformSpecificTooling();
+        await project.ensureReadyForPlatformSpecificTooling();
         expect(project.ios.directory.childFile('Runner/GeneratedPluginRegistrant.h').existsSync(), isFalse);
         expect(project.ios.directory.childFile('Flutter/Generated.xcconfig').existsSync(), isFalse);
       });
       testInMemory('injects plugins', () async {
         final FlutterProject project = aProjectWithIos();
-        project.ensureReadyForPlatformSpecificTooling();
+        await project.ensureReadyForPlatformSpecificTooling();
         expect(project.ios.directory.childFile('Runner/GeneratedPluginRegistrant.h').existsSync(), isTrue);
       });
       testInMemory('generates Xcode configuration', () async {
         final FlutterProject project = aProjectWithIos();
-        project.ensureReadyForPlatformSpecificTooling();
+        await project.ensureReadyForPlatformSpecificTooling();
         expect(project.ios.directory.childFile('Flutter/Generated.xcconfig').existsSync(), isTrue);
       });
     });