diff --git a/.ci/flutter_master.version b/.ci/flutter_master.version index 9544bbfaa5ee..87dfa1e227e0 100644 --- a/.ci/flutter_master.version +++ b/.ci/flutter_master.version @@ -1 +1 @@ -c8f2f166323e5de6fca079c4fca8e959e62fa7d4 +e70534db5a4460d01de2c2575ece049572625526 diff --git a/packages/vector_graphics_compiler/CHANGELOG.md b/packages/vector_graphics_compiler/CHANGELOG.md index 2d2316afdcac..de90ba4cc8c6 100644 --- a/packages/vector_graphics_compiler/CHANGELOG.md +++ b/packages/vector_graphics_compiler/CHANGELOG.md @@ -1,5 +1,7 @@ -## NEXT +## 1.2.4 +* Fix Stack Overflow crashes caused by circular references (masks, patterns, deferred nodes, and clip paths). +* Prevent CPU/Memory Denial of Service (DoS) resource exhaustion from exponential DAG reference expansions (Billion Laughs SVG exploits) by enforcing a strict, cumulative reference expansion safety limit of 1,000. * Updates minimum supported SDK version to Flutter 3.38/Dart 3.10. ## 1.2.3 diff --git a/packages/vector_graphics_compiler/lib/src/svg/constants.dart b/packages/vector_graphics_compiler/lib/src/svg/constants.dart new file mode 100644 index 000000000000..0f1cf3621eb3 --- /dev/null +++ b/packages/vector_graphics_compiler/lib/src/svg/constants.dart @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The maximum number of nested reference expansions allowed in an SVG to prevent DoS exploits. +const int kMaxReferenceExpansions = 1000; + +/// The error message thrown when the nested reference expansions limit is exceeded. +const String kMaxReferenceExpansionsErrorMessage = + 'SVG contains too many nested reference expansions (possible Denial of Service exploit).'; diff --git a/packages/vector_graphics_compiler/lib/src/svg/parser.dart b/packages/vector_graphics_compiler/lib/src/svg/parser.dart index c0d1c5555f0d..d32b060d854d 100644 --- a/packages/vector_graphics_compiler/lib/src/svg/parser.dart +++ b/packages/vector_graphics_compiler/lib/src/svg/parser.dart @@ -21,6 +21,7 @@ import '../vector_instructions.dart'; import 'clipping_optimizer.dart'; import 'color_mapper.dart'; import 'colors.dart'; +import 'constants.dart'; import 'masking_optimizer.dart'; import 'node.dart'; import 'numbers.dart' as numbers show parseDoubleWithUnits; @@ -1678,6 +1679,7 @@ class _Resolver { final Map _drawables = {}; final Map _shaders = {}; final Map> _clips = >{}; + int _deferredExpansionCount = 0; bool _sealed = false; @@ -1702,6 +1704,7 @@ class _Resolver { final pathBuilders = []; PathBuilder? currentPath; + final activeDeferred = {}; void extractPathsFromNode(Node? target) { if (target is PathNode) { final nextPath = PathBuilder.fromPath(target.path); @@ -1716,7 +1719,19 @@ class _Resolver { currentPath!.addPath(nextPath.toPath(reset: false)); } } else if (target is DeferredNode) { - extractPathsFromNode(target.resolver(target.refId)); + _deferredExpansionCount++; + if (_deferredExpansionCount > kMaxReferenceExpansions) { + throw StateError(kMaxReferenceExpansionsErrorMessage); + } + if (!activeDeferred.add(target.refId)) { + // Recursive loop detected. + return; + } + try { + extractPathsFromNode(target.resolver(target.refId)); + } finally { + activeDeferred.remove(target.refId); + } } else if (target is ParentNode) { target.visitChildren(extractPathsFromNode); } diff --git a/packages/vector_graphics_compiler/lib/src/svg/resolver.dart b/packages/vector_graphics_compiler/lib/src/svg/resolver.dart index c36266356f4b..a5abbdfa2c29 100644 --- a/packages/vector_graphics_compiler/lib/src/svg/resolver.dart +++ b/packages/vector_graphics_compiler/lib/src/svg/resolver.dart @@ -9,6 +9,7 @@ import '../geometry/path.dart'; import '../geometry/vertices.dart'; import '../image/image_info.dart'; import '../paint.dart'; +import 'constants.dart'; import 'node.dart'; import 'parser.dart'; import 'visitor.dart'; @@ -19,6 +20,11 @@ import 'visitor.dart'; class ResolvingVisitor extends Visitor { late Rect _bounds; + final Set _activeMasks = {}; + final Set _activeDeferred = {}; + final Set _activePatterns = {}; + int _deferredExpansionCount = 0; + @override Node visitClipNode(ClipNode clipNode, AffineMatrix data) { final AffineMatrix childTransform = clipNode.concatTransform(data); @@ -37,19 +43,31 @@ class ResolvingVisitor extends Visitor { @override Node visitMaskNode(MaskNode maskNode, AffineMatrix data) { - final AttributedNode? resolvedMask = maskNode.resolver(maskNode.maskId); - if (resolvedMask == null) { + _deferredExpansionCount++; + if (_deferredExpansionCount > kMaxReferenceExpansions) { + throw StateError(kMaxReferenceExpansionsErrorMessage); + } + if (!_activeMasks.add(maskNode.maskId)) { + // Recursive loop detected. return maskNode.child.accept(this, data); } - final Node child = maskNode.child.accept(this, data); - final AffineMatrix childTransform = maskNode.concatTransform(data); - final Node mask = resolvedMask.accept(this, childTransform); - - return ResolvedMaskNode( - child: child, - mask: mask, - blendMode: maskNode.blendMode, - ); + try { + final AttributedNode? resolvedMask = maskNode.resolver(maskNode.maskId); + if (resolvedMask == null) { + return maskNode.child.accept(this, data); + } + final Node child = maskNode.child.accept(this, data); + final AffineMatrix childTransform = maskNode.concatTransform(data); + final Node mask = resolvedMask.accept(this, childTransform); + + return ResolvedMaskNode( + child: child, + mask: mask, + blendMode: maskNode.blendMode, + ); + } finally { + _activeMasks.remove(maskNode.maskId); + } } @override @@ -180,17 +198,29 @@ class ResolvingVisitor extends Visitor { @override Node visitDeferredNode(DeferredNode deferredNode, AffineMatrix data) { - final AttributedNode? resolvedNode = deferredNode.resolver( - deferredNode.refId, - ); - if (resolvedNode == null) { + _deferredExpansionCount++; + if (_deferredExpansionCount > kMaxReferenceExpansions) { + throw StateError(kMaxReferenceExpansionsErrorMessage); + } + if (!_activeDeferred.add(deferredNode.refId)) { + // Recursive loop detected. return Node.empty; } - final Node concreteRef = resolvedNode.applyAttributes( - deferredNode.attributes, - replace: true, - ); - return concreteRef.accept(this, data); + try { + final AttributedNode? resolvedNode = deferredNode.resolver( + deferredNode.refId, + ); + if (resolvedNode == null) { + return Node.empty; + } + final Node concreteRef = resolvedNode.applyAttributes( + deferredNode.attributes, + replace: true, + ); + return concreteRef.accept(this, data); + } finally { + _activeDeferred.remove(deferredNode.refId); + } } @override @@ -293,26 +323,38 @@ class ResolvingVisitor extends Visitor { @override Node visitPatternNode(PatternNode patternNode, AffineMatrix data) { - final AttributedNode? resolvedPattern = patternNode.resolver( - patternNode.patternId, - ); - if (resolvedPattern == null) { + _deferredExpansionCount++; + if (_deferredExpansionCount > kMaxReferenceExpansions) { + throw StateError(kMaxReferenceExpansionsErrorMessage); + } + if (!_activePatterns.add(patternNode.patternId)) { + // Recursive loop detected. return patternNode.child.accept(this, data); } - final Node child = patternNode.child.accept(this, data); - final AffineMatrix childTransform = patternNode.concatTransform(data); - final Node pattern = resolvedPattern.accept(this, childTransform); - - return ResolvedPatternNode( - child: child, - pattern: pattern, - x: resolvedPattern.attributes.x?.calculate(0) ?? 0, - y: resolvedPattern.attributes.y?.calculate(0) ?? 0, - width: resolvedPattern.attributes.width!, - height: resolvedPattern.attributes.height!, - transform: data, - id: patternNode.patternId, - ); + try { + final AttributedNode? resolvedPattern = patternNode.resolver( + patternNode.patternId, + ); + if (resolvedPattern == null) { + return patternNode.child.accept(this, data); + } + final Node child = patternNode.child.accept(this, data); + final AffineMatrix childTransform = patternNode.concatTransform(data); + final Node pattern = resolvedPattern.accept(this, childTransform); + + return ResolvedPatternNode( + child: child, + pattern: pattern, + x: resolvedPattern.attributes.x?.calculate(0) ?? 0, + y: resolvedPattern.attributes.y?.calculate(0) ?? 0, + width: resolvedPattern.attributes.width!, + height: resolvedPattern.attributes.height!, + transform: data, + id: patternNode.patternId, + ); + } finally { + _activePatterns.remove(patternNode.patternId); + } } @override diff --git a/packages/vector_graphics_compiler/pubspec.yaml b/packages/vector_graphics_compiler/pubspec.yaml index 7c89a4111110..040e7d5af6f8 100644 --- a/packages/vector_graphics_compiler/pubspec.yaml +++ b/packages/vector_graphics_compiler/pubspec.yaml @@ -2,7 +2,7 @@ name: vector_graphics_compiler description: A compiler to convert SVGs to the binary format used by `package:vector_graphics`. repository: https://github.com/flutter/packages/tree/main/packages/vector_graphics_compiler issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+vector_graphics%22 -version: 1.2.3 +version: 1.2.4 executables: vector_graphics_compiler: diff --git a/packages/vector_graphics_compiler/test/parser_test.dart b/packages/vector_graphics_compiler/test/parser_test.dart index 934cee62ac52..2b9409830b70 100644 --- a/packages/vector_graphics_compiler/test/parser_test.dart +++ b/packages/vector_graphics_compiler/test/parser_test.dart @@ -29,6 +29,346 @@ class _TestOpacityColorMapper implements ColorMapper { } void main() { + test('Exponential DAG expansion triggers DoS protection limit', () { + final svg = + ''' + + + + +${[for (var i = 2; i <= 30; i++) ' '].join('\n')} + + +'''; + + expect( + () => parseWithoutOptimizers(svg), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('SVG contains too many nested reference expansions'), + ), + ), + ); + }); + + test('Exponential DAG clipPath expansion triggers DoS protection limit', () { + final svg = + ''' + + + + +${[for (var i = 2; i <= 30; i++) ' '].join('\n')} + + + +'''; + + expect( + () => parseWithoutOptimizers(svg), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('SVG contains too many nested reference expansions'), + ), + ), + ); + }); + + test( + 'Cumulative clipPath reference expansions trigger DoS protection limit', + () { + final svg = + ''' + + + + +${[for (var i = 2; i <= 8; i++) ' '].join('\n')} + + + + + + +'''; + + expect( + () => parseWithoutOptimizers(svg), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('SVG contains too many nested reference expansions'), + ), + ), + ); + }, + ); + + test('Exponential DAG mask expansion triggers DoS protection limit', () { + final svg = + ''' + + + + +${[for (var i = 2; i <= 30; i++) ' '].join('\n')} + + +'''; + + expect( + () => parseWithoutOptimizers(svg), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('SVG contains too many nested reference expansions'), + ), + ), + ); + }); + + test('Exponential DAG pattern expansion triggers DoS protection limit', () { + final svg = + ''' + + + + +${[for (var i = 2; i <= 30; i++) ' '].join('\n')} + + +'''; + + expect( + () => parseWithoutOptimizers(svg), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('SVG contains too many nested reference expansions'), + ), + ), + ); + }); + + test('Circular Mask Loop Avoidance', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + + + +'''); + + expect(instructions.paths.isNotEmpty, true); + }); + + test('Unreferenced Circular Mask Loop resolves successfully', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + + + + + + + + + + +'''); + + expect(instructions.paths.length, 1); + }); + + test('Multi-hop Referenced Circular Mask Loop Avoidance', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + + + + + + + + + + + + + + + +'''); + + expect(instructions.paths.isNotEmpty, true); + }); + + test('Circular Deferred Node Loop Avoidance', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + + + + +'''); + + expect(instructions.paths.isEmpty, true); + }); + + test('Unreferenced Circular Use Loop resolves successfully', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + + + + + + +'''); + + expect(instructions.paths.length, 1); + }); + + test('Multi-hop Referenced Circular Use Loop Avoidance', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + + + + + + + + + +'''); + + expect(instructions.paths.isEmpty, true); + }); + + test('Circular Pattern Loop Avoidance', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + +'''); + + expect(instructions.paths.isNotEmpty, true); + }); + + test('Circular ClipPath Loop Avoidance', () { + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + + + + + + + +'''); + expect(instructions.paths.length, 1); + expect( + instructions.commands.any((c) => c.type == DrawCommandType.clip), + false, + ); + }); + + test('Shared DAG Sibling Node (No Loop) resolves successfully', () { + // Case 1: Identical positions. The paths are geometrically identical, + // so they are deduplicated in the unique paths list, but drawn twice in the command list. + final VectorInstructions instructions = parseWithoutOptimizers(''' + + + + + + + + + + + + + + + + +'''); + + expect(instructions.paths.length, 1); + expect( + instructions.commands.where((c) => c.type == DrawCommandType.path).length, + 2, + ); + + // Case 2: Distinct positions. The paths are transformed differently, + // so they are distinct paths and both exist in the unique paths list. + final VectorInstructions distinctInstructions = parseWithoutOptimizers(''' + + + + + + + + + + + + + + + + +'''); + + expect(distinctInstructions.paths.length, 2); + expect( + distinctInstructions.commands + .where((c) => c.type == DrawCommandType.path) + .length, + 2, + ); + }); + test('Reuse ID self-referentially', () { final VectorInstructions instructions = parseWithoutOptimizers(''' diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md index 4bc8c56d7809..55aeacaf445b 100644 --- a/script/tool/CHANGELOG.md +++ b/script/tool/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.14.2 + +* Ensures that pub commands use `flutter` or `dart` depending on whether the + package requires Flutter for analysis and publishing, to support + non-Flutter-based repositories. +* Adds `--skip-if-not-supporting-dart-version` to support package constraints + via Dart versions rather than Flutter versions. + ## 0.14.1 * Adds `min_dart` as an alternative to `min_flutter` in tool configuration. diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart index 91f247eed2c2..9f32195e9bdc 100644 --- a/script/tool/lib/src/analyze_command.dart +++ b/script/tool/lib/src/analyze_command.dart @@ -13,6 +13,7 @@ import 'common/gradle.dart'; import 'common/output_utils.dart'; import 'common/package_looping_command.dart'; import 'common/plugin_utils.dart'; +import 'common/pub_utils.dart'; import 'common/repository_package.dart'; import 'common/xcode.dart'; @@ -357,12 +358,13 @@ class AnalyzeCommand extends PackageLoopingCommand { } Future _runPubCommand(RepositoryPackage package, String command) async { - final int exitCode = await processRunner.runAndStream( - flutterCommand, - ['pub', command], - workingDir: package.directory, + return runPubCommand( + [command], + package, + processRunner, + platform, + dartSdkPathOverride: _dartBinaryPath, ); - return exitCode == 0; } /// Runs Gradle lint analysis for the given package, and returns the result diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart index 53c885bcf6e7..04a284f7b720 100644 --- a/script/tool/lib/src/common/package_looping_command.dart +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -95,11 +95,20 @@ abstract class PackageLoopingCommand extends PackageCommand { 'the provided version, or a Dart version newer than the ' 'corresponding Dart version.', ); + argParser.addOption( + _skipByDartVersionArg, + help: + 'Skip any packages that require a Dart version newer than the ' + 'provided version.', + ); } static const String _skipByFlutterVersionArg = 'skip-if-not-supporting-flutter-version'; + static const String _skipByDartVersionArg = + 'skip-if-not-supporting-dart-version'; + /// Packages that had at least one [logWarning] call. final Set _packagesWithWarnings = {}; @@ -289,9 +298,15 @@ abstract class PackageLoopingCommand extends PackageCommand { final Version? minFlutterVersion = minFlutterVersionArg.isEmpty ? null : Version.parse(minFlutterVersionArg); - final Version? minDartVersion = minFlutterVersion == null - ? null - : getDartSdkForFlutterSdk(minFlutterVersion); + + // Use the explicit Dart min if provided, otherwise fall back to the + // version corresponding to the Flutter min if that was provided. + final String minDartVersionArg = getStringArg(_skipByDartVersionArg); + final Version? minDartVersion = minDartVersionArg.isNotEmpty + ? Version.parse(minDartVersionArg) + : (minFlutterVersion == null + ? null + : getDartSdkForFlutterSdk(minFlutterVersion)); final runStart = DateTime.now(); diff --git a/script/tool/lib/src/common/pub_utils.dart b/script/tool/lib/src/common/pub_utils.dart index 963a548aef68..7bed2da631b6 100644 --- a/script/tool/lib/src/common/pub_utils.dart +++ b/script/tool/lib/src/common/pub_utils.dart @@ -12,27 +12,42 @@ import 'repository_package.dart'; /// Runs either `dart pub get` or `flutter pub get` in [package], depending on /// the package type. /// -/// If [alwaysUseFlutter] is true, it will use `flutter pub get` regardless. -/// This can be useful, for instance, to get the `flutter`-default behavior -/// of fetching example packages as well. -/// /// If [streamOutput] is false, output will only be printed if the command /// fails. Future runPubGet( RepositoryPackage package, ProcessRunner processRunner, Platform platform, { - bool alwaysUseFlutter = false, bool streamOutput = true, }) async { - // Running `dart pub get` on a Flutter package can fail if a non-Flutter Dart - // is first in the path, so use `flutter pub get` for any Flutter package. - final bool useFlutter = alwaysUseFlutter || package.requiresFlutter(); - final command = useFlutter - ? (platform.isWindows ? 'flutter.bat' : 'flutter') - : 'dart'; - final args = ['pub', 'get']; + return runPubCommand( + ['get'], + package, + processRunner, + platform, + streamOutput: streamOutput, + ); +} +/// Runs a pub command with the given arguments in [package], +/// using either 'dart' or 'flutter' depending on the package type. +/// +/// If [streamOutput] is false, output will only be printed if the command +/// fails. +Future runPubCommand( + List commandArgs, + RepositoryPackage package, + ProcessRunner processRunner, + Platform platform, { + bool streamOutput = true, + String? dartSdkPathOverride, +}) async { + final String command = _pubCommand( + package, + platform, + dartSdkPathOverride: dartSdkPathOverride, + ); + final args = ['pub', ...commandArgs]; final int exitCode; if (streamOutput) { exitCode = await processRunner.runAndStream( @@ -53,3 +68,32 @@ Future runPubGet( } return exitCode == 0; } + +/// Starts a pub command with the given arguments in [package], +/// using either 'dart' or 'flutter' depending on the package type, and returns +/// a process that can be used to wait for completion and stream output. +/// +/// If no output capturing is necessary, prefer [runPubCommand]. +Future startPubCommand( + List commandArgs, + RepositoryPackage package, + ProcessRunner processRunner, + Platform platform, +) async { + return processRunner.start(_pubCommand(package, platform), [ + 'pub', + ...commandArgs, + ], workingDirectory: package.directory); +} + +String _pubCommand( + RepositoryPackage package, + Platform platform, { + String? dartSdkPathOverride, +}) { + // Running `dart pub get` on a Flutter package can fail if a non-Flutter Dart + // is first in the path, so use `flutter pub get` for any Flutter package. + return package.requiresFlutter() + ? (platform.isWindows ? 'flutter.bat' : 'flutter') + : (dartSdkPathOverride ?? 'dart'); +} diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart index dbb6a0711e83..2e28b39e572e 100644 --- a/script/tool/lib/src/publish_check_command.dart +++ b/script/tool/lib/src/publish_check_command.dart @@ -163,10 +163,11 @@ class PublishCheckCommand extends PackageLoopingCommand { await _fetchExampleDeps(package); print('Running pub publish --dry-run:'); - final io.Process process = await processRunner.start( - flutterCommand, - ['pub', 'publish', '--', '--dry-run'], - workingDirectory: package.directory, + final io.Process process = await startPubCommand( + ['publish', '--dry-run'], + package, + processRunner, + platform, ); final outputBuffer = StringBuffer(); diff --git a/script/tool/lib/src/publish_command.dart b/script/tool/lib/src/publish_command.dart index 41d982d4d9d3..54b5a4dda1da 100644 --- a/script/tool/lib/src/publish_command.dart +++ b/script/tool/lib/src/publish_command.dart @@ -477,10 +477,11 @@ Safe to ignore if the package is deleted in this commit. _ensureValidPubCredential(); } - final io.Process publish = await processRunner.start( - flutterCommand, - ['pub', 'publish', ..._publishFlags], - workingDirectory: package.directory, + final io.Process publish = await startPubCommand( + ['publish', ..._publishFlags], + package, + processRunner, + platform, ); publish.stdout.transform(utf8.decoder).listen((String data) => print(data)); publish.stderr.transform(utf8.decoder).listen((String data) => print(data)); diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml index 1752a98250f0..ed259156675f 100644 --- a/script/tool/pubspec.yaml +++ b/script/tool/pubspec.yaml @@ -1,7 +1,7 @@ name: flutter_plugin_tools description: Productivity and CI utils for Flutter team package repositories. repository: https://github.com/flutter/packages/tree/main/script/tool -version: 0.14.1 +version: 0.14.2 dependencies: args: ^2.1.0 diff --git a/script/tool/test/analyze_command_test.dart b/script/tool/test/analyze_command_test.dart index 9e0b2d6a0752..1f5da3ec3d32 100644 --- a/script/tool/test/analyze_command_test.dart +++ b/script/tool/test/analyze_command_test.dart @@ -136,6 +136,8 @@ void main() { group('dart analyze', () { test('analyzes all packages', () async { + // Create a non-Flutter Dart package and a Flutter plugin to make sure + // the right command is used for each. final RepositoryPackage package1 = createFakePackage('a', packagesDir); final RepositoryPackage plugin2 = createFakePlugin('b', packagesDir); @@ -144,7 +146,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], package1.path), + ProcessCall('dart', const ['pub', 'get'], package1.path), ProcessCall('dart', const [ 'analyze', '--fatal-infos', @@ -158,7 +160,7 @@ void main() { ); }); - test('skips flutter pub get for examples', () async { + test('skips pub get for examples', () async { final RepositoryPackage plugin1 = createFakePlugin('a', packagesDir); await runCapturingPrint(runner, ['analyze']); @@ -175,7 +177,7 @@ void main() { ); }); - test('runs flutter pub get for non-example subpackages', () async { + test('runs pub get for non-example subpackages', () async { final RepositoryPackage mainPackage = createFakePackage('a', packagesDir); final Directory otherPackagesDir = mainPackage.directory.childDirectory( 'other_packages', @@ -194,18 +196,9 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', const [ - 'pub', - 'get', - ], mainPackage.path), - ProcessCall('flutter', const [ - 'pub', - 'get', - ], subpackage1.path), - ProcessCall('flutter', const [ - 'pub', - 'get', - ], subpackage2.path), + ProcessCall('dart', const ['pub', 'get'], mainPackage.path), + ProcessCall('dart', const ['pub', 'get'], subpackage1.path), + ProcessCall('dart', const ['pub', 'get'], subpackage2.path), ProcessCall('dart', const [ 'analyze', '--fatal-infos', @@ -225,7 +218,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], package.path), + ProcessCall('dart', const ['pub', 'get'], package.path), ProcessCall('dart', const [ 'analyze', '--fatal-infos', @@ -255,7 +248,7 @@ void main() { }); test( - 'does not run flutter pub get for non-example subpackages with --lib-only', + 'does not run pub get for non-example subpackages with --lib-only', () async { final RepositoryPackage mainPackage = createFakePackage( 'a', @@ -272,10 +265,7 @@ void main() { expect( processRunner.recordedCalls, orderedEquals([ - ProcessCall('flutter', const [ - 'pub', - 'get', - ], mainPackage.path), + ProcessCall('dart', const ['pub', 'get'], mainPackage.path), ProcessCall('dart', const [ 'analyze', '--fatal-infos', diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart index 1589e748b435..49b1efd5a275 100644 --- a/script/tool/test/common/package_looping_command_test.dart +++ b/script/tool/test/common/package_looping_command_test.dart @@ -501,7 +501,51 @@ skip/b ); }); - test('skips unsupported Dart versions when requested', () async { + test( + 'skips unsupported Dart versions when requested implicitly by Flutter version', + () async { + final RepositoryPackage excluded = createFakePackage( + 'excluded_package', + packagesDir, + dartConstraint: '>=2.18.0 <4.0.0', + ); + final RepositoryPackage included = createFakePackage( + 'a_package', + packagesDir, + ); + + final TestPackageLoopingCommand command = createTestCommand( + packageLoopingType: PackageLoopingType.includeAllSubpackages, + hasLongOutput: false, + ); + final List output = await runCommand( + command, + arguments: [ + '--skip-if-not-supporting-flutter-version=3.0.0', // Flutter 3.0.0 -> Dart 2.17.0 + ], + ); + + expect( + command.checkedPackages, + unorderedEquals([ + included.path, + getExampleDir(included).path, + ]), + ); + expect(command.checkedPackages, isNot(contains(excluded.path))); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for a_package...$_endColor', + '${_startHeadingColor}Running for excluded_package...$_endColor', + '$_startSkipColor SKIPPING: Does not support Dart 2.17.0$_endColor', + ]), + ); + }, + ); + + test('skips unsupported Dart versions when requested explicitly', () async { final RepositoryPackage excluded = createFakePackage( 'excluded_package', packagesDir, @@ -518,9 +562,7 @@ skip/b ); final List output = await runCommand( command, - arguments: [ - '--skip-if-not-supporting-flutter-version=3.0.0', // Flutter 3.0.0 -> Dart 2.17.0 - ], + arguments: ['--skip-if-not-supporting-dart-version=2.17.0'], ); expect( diff --git a/script/tool/test/common/pub_utils_test.dart b/script/tool/test/common/pub_utils_test.dart index d18b1290a239..2dcc3627c9b5 100644 --- a/script/tool/test/common/pub_utils_test.dart +++ b/script/tool/test/common/pub_utils_test.dart @@ -53,23 +53,6 @@ void main() { ); }); - test('runs with Flutter for a Dart package when requested', () async { - final RepositoryPackage package = createFakePackage( - 'a_package', - packagesDir, - ); - final platform = MockPlatform(); - - await runPubGet(package, processRunner, platform, alwaysUseFlutter: true); - - expect( - processRunner.recordedCalls, - orderedEquals([ - ProcessCall('flutter', const ['pub', 'get'], package.path), - ]), - ); - }); - test('uses the correct Flutter command on Windows', () async { final RepositoryPackage package = createFakePackage( 'a_package', diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart index 7b1b265e63a4..a97737517615 100644 --- a/script/tool/test/publish_check_command_test.dart +++ b/script/tool/test/publish_check_command_test.dart @@ -71,13 +71,11 @@ void main() { ProcessCall('flutter', const [ 'pub', 'publish', - '--', '--dry-run', ], plugin1.path), ProcessCall('flutter', const [ 'pub', 'publish', - '--', '--dry-run', ], plugin2.path), ]), @@ -117,14 +115,12 @@ void main() { ProcessCall('flutter', const [ 'pub', 'publish', - '--', '--dry-run', ], plugin1.path), // plugin2 has no examples, so there's no extra 'dart pub get' calls. ProcessCall('flutter', const [ 'pub', 'publish', - '--', '--dry-run', ], plugin2.path), ]), @@ -329,7 +325,8 @@ void main() { runner = configureRunner(httpClient: mockClient); - processRunner.mockProcessesForExecutable['flutter'] = [ + processRunner.mockProcessesForExecutable['dart'] = [ + FakeProcessInfo(MockProcess(), ['pub', 'get']), FakeProcessInfo( MockProcess(exitCode: 1, stdout: 'Some error from pub'), ['pub', 'publish'], @@ -355,10 +352,9 @@ void main() { expect( processRunner.recordedCalls, contains( - ProcessCall('flutter', const [ + ProcessCall('dart', const [ 'pub', 'publish', - '--', '--dry-run', ], package.path), ), @@ -426,10 +422,9 @@ void main() { expect( processRunner.recordedCalls, contains( - ProcessCall('flutter', const [ + ProcessCall('dart', const [ 'pub', 'publish', - '--', '--dry-run', ], package.path), ), @@ -496,10 +491,9 @@ void main() { 'run', 'tool/pre_publish.dart', ], package.directory.path), - ProcessCall('flutter', const [ + ProcessCall('dart', const [ 'pub', 'publish', - '--', '--dry-run', ], package.directory.path), ]), diff --git a/script/tool/test/publish_command_test.dart b/script/tool/test/publish_command_test.dart index 0438b8cffeda..f32119b4f230 100644 --- a/script/tool/test/publish_command_test.dart +++ b/script/tool/test/publish_command_test.dart @@ -285,6 +285,29 @@ void main() { ); }); + test('uses dart rather than flutter for non-Flutter packages', () async { + final RepositoryPackage package = createFakePackage( + 'foo', + packagesDir, + examples: [], + ); + + await runCapturingPrint(commandRunner, [ + 'publish', + '--packages=foo', + ]); + + expect( + processRunner.recordedCalls, + contains( + ProcessCall('dart', const [ + 'pub', + 'publish', + ], package.directory.path), + ), + ); + }); + test('forwards --pub-publish-flags to pub publish', () async { final RepositoryPackage plugin = createFakePlugin( 'foo',