diff --git a/packages/pigeon/CHANGELOG.md b/packages/pigeon/CHANGELOG.md index 86575bae5d6e..2b49cf91cc8b 100644 --- a/packages/pigeon/CHANGELOG.md +++ b/packages/pigeon/CHANGELOG.md @@ -1,3 +1,7 @@ +## 28.1.0 + +* [swift] Adds support for multiple output locations in `swiftOut` and `--swift_out`. + ## 28.0.0 * **Breaking Change** Updates Kotlin and Swift generators to generate `suspend` functions and `async throws` signatures for `@FlutterApi` methods by default, and for `@HostApi` methods annotated with `@async`. diff --git a/packages/pigeon/lib/src/generator_tools.dart b/packages/pigeon/lib/src/generator_tools.dart index 6259b92c5ce3..2b33a651c88c 100644 --- a/packages/pigeon/lib/src/generator_tools.dart +++ b/packages/pigeon/lib/src/generator_tools.dart @@ -15,7 +15,7 @@ import 'generator.dart'; /// The current version of pigeon. /// /// This must match the version in pubspec.yaml. -const String pigeonVersion = '28.0.0'; +const String pigeonVersion = '28.1.0'; /// Default plugin package name. const String defaultPluginPackageName = 'dev.flutter.pigeon'; diff --git a/packages/pigeon/lib/src/pigeon_lib.dart b/packages/pigeon/lib/src/pigeon_lib.dart index 4d2418536c35..0bdf35b7574a 100644 --- a/packages/pigeon/lib/src/pigeon_lib.dart +++ b/packages/pigeon/lib/src/pigeon_lib.dart @@ -294,8 +294,26 @@ class PigeonOptions { /// Options that control how Java will be generated. final JavaOptions? javaOptions; - /// Path to the swift file that will be generated. - final String? swiftOut; + /// Path to the swift file(s) that will be generated. + /// + /// Can be either a [String] for a single file, or an [Iterable] for + /// multiple files. + final Object? swiftOut; + + /// Returns all output paths for Swift from [swiftOut]. + Iterable? get swiftOutPaths { + final Object? out = swiftOut; + if (out == null) { + return null; + } + if (out is String) { + return [out]; + } + if (out is Iterable) { + return out.whereType(); + } + return null; + } /// Options that control how Swift will be generated. final SwiftOptions? swiftOptions; @@ -361,7 +379,9 @@ class PigeonOptions { javaOptions: map.containsKey('javaOptions') ? JavaOptions.fromMap(map['javaOptions']! as Map) : null, - swiftOut: map['swiftOut'] as String?, + swiftOut: map['swiftOut'] is Iterable + ? (map['swiftOut']! as Iterable).cast().toList() + : map['swiftOut'] as String?, swiftOptions: map.containsKey('swiftOptions') ? SwiftOptions.fromList(map['swiftOptions']! as Map) : null, @@ -520,9 +540,9 @@ ${_argParser.usage}'''; 'java_use_generated_annotation', help: 'Adds the java.annotation.Generated annotation to the output.', ) - ..addOption( + ..addMultiOption( 'swift_out', - help: 'Path to generated Swift file (.swift).', + help: 'Path to generated Swift file(s) (.swift).', aliases: const ['experimental_swift_out'], ) ..addOption( @@ -596,6 +616,7 @@ ${_argParser.usage}'''; // get set in the `run` function to accommodate users that are using the // `configurePigeon` function. final ArgResults results = _argParser.parse(args); + final swiftOuts = results['swift_out'] as List; final opts = PigeonOptions( input: results['input'] as String?, @@ -609,7 +630,9 @@ ${_argParser.usage}'''; package: results['java_package'] as String?, useGeneratedAnnotation: results['java_use_generated_annotation'] as bool?, ), - swiftOut: results['swift_out'] as String?, + swiftOut: results.wasParsed('swift_out') + ? (swiftOuts.length == 1 ? swiftOuts.first : swiftOuts) + : null, kotlinOut: results['kotlin_out'] as String?, kotlinOptions: KotlinOptions( package: results['kotlin_package'] as String?, diff --git a/packages/pigeon/lib/src/pigeon_lib_internal.dart b/packages/pigeon/lib/src/pigeon_lib_internal.dart index e6ea9a96c9a8..e74f09303b6c 100644 --- a/packages/pigeon/lib/src/pigeon_lib_internal.dart +++ b/packages/pigeon/lib/src/pigeon_lib_internal.dart @@ -67,11 +67,11 @@ class InternalPigeonOptions { javaOut: options.javaOut!, copyrightHeader: copyrightHeader, ), - swiftOptions = options.swiftOut == null + swiftOptions = (options.swiftOutPaths == null || options.swiftOutPaths!.isEmpty) ? null : InternalSwiftOptions.fromSwiftOptions( options.swiftOptions ?? const SwiftOptions(), - swiftOut: options.swiftOut!, + swiftOuts: options.swiftOutPaths, copyrightHeader: copyrightHeader, ), kotlinOptions = options.kotlinOut == null @@ -405,12 +405,39 @@ class SwiftGeneratorAdapter implements GeneratorAdapter { return; } const generator = SwiftGenerator(); - generator.generate(options.swiftOptions!, root, sink, dartPackageName: options.dartPackageName); + final List outputs = options.swiftOptions!.allSwiftOuts.toList(); + if (outputs.isEmpty) { + generator.generate( + options.swiftOptions!, + root, + sink, + dartPackageName: options.dartPackageName, + ); + return; + } + final buffer = StringBuffer(); + generator.generate( + options.swiftOptions!, + root, + buffer, + dartPackageName: options.dartPackageName, + ); + final content = buffer.toString(); + sink.write(content); + for (final String outputPath in outputs.skip(1)) { + if (outputPath == 'stdout') { + stdout.write(content); + } else { + final file = File(path.posix.join(options.basePath ?? '', outputPath)); + file.createSync(recursive: true); + file.writeAsStringSync(content); + } + } } @override IOSink? shouldGenerate(InternalPigeonOptions options, FileType _) => - _openSink(options.swiftOptions?.swiftOut, basePath: options.basePath ?? ''); + _openSink(options.swiftOptions?.allSwiftOuts.firstOrNull, basePath: options.basePath ?? ''); @override List validate(InternalPigeonOptions options, Root root) { diff --git a/packages/pigeon/lib/src/swift/swift_generator.dart b/packages/pigeon/lib/src/swift/swift_generator.dart index 17f96ca56c48..2e1017c75eb7 100644 --- a/packages/pigeon/lib/src/swift/swift_generator.dart +++ b/packages/pigeon/lib/src/swift/swift_generator.dart @@ -84,6 +84,7 @@ class InternalSwiftOptions extends InternalOptions { const InternalSwiftOptions({ this.copyrightHeader, required this.swiftOut, + this.swiftOuts, this.fileSpecificClassNameComponent, this.errorClassName, this.includeErrorClass = true, @@ -92,22 +93,38 @@ class InternalSwiftOptions extends InternalOptions { /// Creates InternalSwiftOptions from SwiftOptions. InternalSwiftOptions.fromSwiftOptions( SwiftOptions options, { - required this.swiftOut, + Iterable? swiftOuts, + String? swiftOut, Iterable? copyrightHeader, }) : copyrightHeader = options.copyrightHeader ?? copyrightHeader, fileSpecificClassNameComponent = options.fileSpecificClassNameComponent ?? - swiftOut.split('/').lastOrNull?.split('.').firstOrNull ?? + (swiftOuts?.firstOrNull ?? swiftOut ?? '') + .split('/') + .lastOrNull + ?.split('.') + .firstOrNull ?? '', errorClassName = options.errorClassName, - includeErrorClass = options.includeErrorClass; + includeErrorClass = options.includeErrorClass, + swiftOut = swiftOut ?? swiftOuts?.firstOrNull ?? '', + swiftOuts = swiftOuts ?? (swiftOut != null ? [swiftOut] : const []); /// A copyright header that will get prepended to generated code. final Iterable? copyrightHeader; /// Path to the swift file that will be generated. + /// + /// If multiple output paths were specified, this contains the first path. final String swiftOut; + /// Paths to all swift files that will be generated. + final Iterable? swiftOuts; + + /// Returns all output paths for Swift. + Iterable get allSwiftOuts => + swiftOuts ?? (swiftOut.isNotEmpty ? [swiftOut] : const []); + /// A String to augment class names to avoid cross file collisions. final String? fileSpecificClassNameComponent; diff --git a/packages/pigeon/pubspec.yaml b/packages/pigeon/pubspec.yaml index 0f754e2878fe..517720894bf8 100644 --- a/packages/pigeon/pubspec.yaml +++ b/packages/pigeon/pubspec.yaml @@ -2,7 +2,7 @@ name: pigeon description: Code generator tool to make communication between Flutter and the host platform type-safe and easier. repository: https://github.com/flutter/packages/tree/main/packages/pigeon issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pigeon%22 -version: 28.0.0 # This must match the version in lib/src/generator_tools.dart +version: 28.1.0 # This must match the version in lib/src/generator_tools.dart environment: sdk: ^3.11.0 diff --git a/packages/pigeon/test/pigeon_lib_test.dart b/packages/pigeon/test/pigeon_lib_test.dart index e8df30b2ec13..e1075823e901 100644 --- a/packages/pigeon/test/pigeon_lib_test.dart +++ b/packages/pigeon/test/pigeon_lib_test.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:path/path.dart' as path; import 'package:pigeon/src/ast.dart'; import 'package:pigeon/src/generator_tools.dart'; import 'package:pigeon/src/pigeon_lib.dart'; @@ -95,6 +96,21 @@ void main() { expect(opts.swiftOut, equals('Foo.swift')); }); + test('parse args - multiple swift_out', () { + final PigeonOptions opts = Pigeon.parseArgs([ + '--swift_out', + 'Foo.swift', + '--swift_out', + 'Bar.swift', + ]); + expect(opts.swiftOut, equals(['Foo.swift', 'Bar.swift'])); + }); + + test('parse args - comma-separated swift_out', () { + final PigeonOptions opts = Pigeon.parseArgs(['--swift_out', 'Foo.swift,Bar.swift']); + expect(opts.swiftOut, equals(['Foo.swift', 'Bar.swift'])); + }); + test('parse args - kotlin_out', () { final PigeonOptions opts = Pigeon.parseArgs(['--kotlin_out', 'Foo.kt']); expect(opts.kotlinOut, equals('Foo.kt')); @@ -1406,6 +1422,115 @@ class Message { expect(options.cppOptions?.headerIncludePath, 'Header.path'); }); + test('@ConfigurePigeon swiftOut single', () { + const code = ''' +@ConfigurePigeon(PigeonOptions( + swiftOut: 'Foo.swift', +)) +class Message { + int? id; +} +'''; + + final ParseResults results = parseSource(code); + final PigeonOptions options = PigeonOptions.fromMap(results.pigeonOptions!); + expect(options.swiftOut, 'Foo.swift'); + expect(options.swiftOutPaths, ['Foo.swift']); + }); + + test('@ConfigurePigeon swiftOut multiple', () { + const code = ''' +@ConfigurePigeon(PigeonOptions( + swiftOut: ['Foo.swift', 'Bar.swift'], +)) +class Message { + int? id; +} +'''; + + final ParseResults results = parseSource(code); + final PigeonOptions options = PigeonOptions.fromMap(results.pigeonOptions!); + expect(options.swiftOut, ['Foo.swift', 'Bar.swift']); + expect(options.swiftOutPaths, ['Foo.swift', 'Bar.swift']); + }); + + test('multiple swiftOut generation', () async { + final completer = Completer(); + const code = ''' +@HostApi() +abstract class Api { + void doSomething(); +} +'''; + final Directory dir = Directory.systemTemp.createTempSync(); + try { + final input = File(path.join(dir.path, 'input.dart')); + input.writeAsStringSync(code); + final String swift1 = path.join(dir.path, 'one.swift'); + final String swift2 = path.join(dir.path, 'two.swift'); + final int result = await Pigeon.runWithOptions( + PigeonOptions( + input: input.path, + swiftOut: [swift1, swift2], + dartOut: path.join(dir.path, 'out.dart'), + dartPackageName: 'test_package', + ), + ); + expect(result, equals(0)); + expect(File(swift1).existsSync(), isTrue); + expect(File(swift2).existsSync(), isTrue); + final String content1 = File(swift1).readAsStringSync(); + final String content2 = File(swift2).readAsStringSync(); + expect(content1, isNotEmpty); + expect(content1, equals(content2)); + expect(content1, contains('protocol Api')); + completer.complete(); + } finally { + dir.deleteSync(recursive: true); + } + await completer.future; + }); + + test('multiple swift_out args generation', () async { + final completer = Completer(); + const code = ''' +@HostApi() +abstract class Api { + void doSomething(); +} +'''; + final Directory dir = Directory.systemTemp.createTempSync(); + try { + final input = File(path.join(dir.path, 'input.dart')); + input.writeAsStringSync(code); + final String swift1 = path.join(dir.path, 'one.swift'); + final String swift2 = path.join(dir.path, 'two.swift'); + final int result = await Pigeon.run([ + '--input', + input.path, + '--dart_out', + path.join(dir.path, 'out.dart'), + '--package_name', + 'test_package', + '--swift_out', + swift1, + '--swift_out', + swift2, + ]); + expect(result, equals(0)); + expect(File(swift1).existsSync(), isTrue); + expect(File(swift2).existsSync(), isTrue); + final String content1 = File(swift1).readAsStringSync(); + final String content2 = File(swift2).readAsStringSync(); + expect(content1, isNotEmpty); + expect(content1, equals(content2)); + completer.complete(); + } finally { + dir.deleteSync(recursive: true); + } + await completer.future; + }); + test('return nullable', () { const code = ''' @HostApi() diff --git a/packages/pigeon/tool/shared/generation.dart b/packages/pigeon/tool/shared/generation.dart index 68930c0ef30d..cc5c6fcef49c 100644 --- a/packages/pigeon/tool/shared/generation.dart +++ b/packages/pigeon/tool/shared/generation.dart @@ -212,7 +212,7 @@ Future runPigeon({ bool kotlinIncludeErrorClass = true, bool kotlinUseGeneratedAnnotation = false, bool swiftIncludeErrorClass = true, - String? swiftOut, + Object? swiftOut, String? swiftErrorClassName, String? cppHeaderOut, String? cppSourceOut,