Skip to content
71 changes: 71 additions & 0 deletions packages/flutter_tools/lib/src/commands/create.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import 'package:meta/meta.dart';
import 'package:unified_analytics/unified_analytics.dart';
import 'package:yaml/yaml.dart';

import '../android/gradle_utils.dart' as gradle;
import '../base/common.dart';
Expand All @@ -14,6 +15,7 @@ import '../base/terminal.dart';
import '../base/utils.dart';
import '../base/version.dart';
import '../base/version_range.dart';
import '../cache.dart';
import '../convert.dart';
import '../dart/pub.dart';
import '../darwin/darwin.dart';
Expand Down Expand Up @@ -545,6 +547,8 @@ class CreateCommand extends FlutterCommand with CreateBase {
pubContext = PubContext.createPackage;
}

_generatePubspecLock(relativeDir);

if (shouldCallPubGet) {
final FlutterProject project = FlutterProject.fromDirectory(relativeDir);
await pub.get(
Expand Down Expand Up @@ -1351,3 +1355,70 @@ List<String>? _getBuildGradleConfigurationFilePaths(
}
return buildGradleConfigurationFilePaths;
}

/// Generate a pubspec.lock file that locks the versions of all dependencies to
/// the exact versions the Flutter SDK is tested with.
///
/// This ensures that a breaking change accidentally published to one of these
/// packages that the Flutter SDK depends on cannot break the `flutter create`
/// command.
void _generatePubspecLock(Directory directory) {
final FileSystem fs = directory.fileSystem;
final String flutterRoot = Cache.flutterRoot!;
final flutterPubspecLock =
loadYaml(fs.file(fs.path.join(flutterRoot, 'pubspec.lock')).readAsStringSync()) as YamlMap;

final flutterPackages = flutterPubspecLock['packages'] as YamlMap;

final packages = <String, Object?>{
for (final package in gatherSdkPackageDependencies(directory))
package: flutterPackages[package],
};

/// We are writing json which is valid yaml. Pub will rewrite this file as pretty yaml after resolution.
directory
.childFile('pubspec.lock')
.writeAsStringSync(const JsonEncoder.withIndent(' ').convert({'packages': packages}));
Comment thread
sigurdm marked this conversation as resolved.
}

/// Find the package names of external dependencies from the SDK packages that
/// the package in [directory] depends on.
List<String> gatherSdkPackageDependencies(Directory directory) {
final sdkPackages = <String>[];
final FileSystem fs = directory.fileSystem;
final pubspecYaml = loadYaml(directory.childFile('pubspec.yaml').readAsStringSync()) as YamlMap;

for (final MapEntry<dynamic, dynamic> dependency
in (pubspecYaml['dependencies'] as YamlMap).entries) {
final descriptor = dependency.value as Object?;
if (descriptor is YamlMap && descriptor['sdk'] == 'flutter') {
// a flutter dependency.
final name = dependency.key as String;
sdkPackages.add(name);
}
}

final result = <String>{};
// Initialized by FlutterCommandRunner on startup.
// So it is safe to access it here.
final String flutterRoot = Cache.flutterRoot!;
for (final sdkPackage in sdkPackages) {
final pubspecYaml =
loadYaml(
fs
.file(fs.path.join(flutterRoot, 'packages', sdkPackage, 'pubspec.yaml'))
.readAsStringSync(),
)
as YamlMap;
for (final MapEntry<dynamic, dynamic> dependency
in (pubspecYaml['dependencies'] as YamlMap).entries) {
final descriptor = dependency.value as Object?;
if (descriptor is String) {
// a hosted dependency.
final name = dependency.key as String;
result.add(name);
}
}
}
return result.toList();
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,17 @@ void main() {
),
globals.fs.path.join('usr', 'local', 'bin', 'adb'),
globals.fs.path.join('Android', 'platform-tools', 'adb.exe'),
globals.fs.path.join('flutter', 'pubspec.lock'),
globals.fs.path.join('flutter', 'version'),
];
for (final filePath in filePaths) {
globals.fs.file(filePath).createSync(recursive: true);
final File file = globals.fs.file(filePath);
file.createSync(recursive: true);
if (filePath.endsWith('pubspec.yaml')) {
file.writeAsStringSync('dependencies: {}\n');
} else if (filePath.endsWith('pubspec.lock')) {
file.writeAsStringSync('packages: {}\n');
}
}
final templatePaths = <String>[
globals.fs.path.join('flutter', 'packages', 'flutter_tools', 'templates', 'app'),
Expand Down Expand Up @@ -157,7 +165,7 @@ void main() {
globals.fs
.directory(templatePath)
.childFile('pubspec.yaml.tmpl')
.writeAsStringSync('name: my_app');
.writeAsStringSync('name: my_app\ndependencies: {}\n');
}
// Set up enough of the packages to satisfy the templating code.
final File packagesFile = globals.fs.file(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5125,6 +5125,30 @@ To keep the default AGP version $templateAndroidGradlePluginVersion, download a
expect(logger.statusText, isNot(contains(r' $ cd')));
}, overrides: {Logger: () => logger});

testUsingContext(
'default project locks known dependencies to known versions with pubspec.lock',
() async {
await _createProject(projectDir, <String>['--no-pub'], <String>['pubspec.lock']);
Comment thread
sigurdm marked this conversation as resolved.

final projectPubspecLock =
jsonDecode(await projectDir.childFile('pubspec.lock').readAsString()) as Map;
final projectPackages = projectPubspecLock['packages'] as Map;
expect(projectPackages, isNotEmpty);

final flutterPubspecLock =
loadYaml(
globals.fs
.file(globals.fs.path.join(getFlutterRoot(), 'pubspec.lock'))
.readAsStringSync(),
)
as YamlMap;
final flutterPackages = flutterPubspecLock['packages'] as YamlMap;
for (final MapEntry<Object?, Object?> p in projectPackages.entries) {
expect(flutterPackages[p.key], p.value);
}
},
);

testUsingContext('generated pubspec uses default build number (+1) for empty app', () async {
await projectDir.create(recursive: true);

Expand Down