Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/lib/src/generator_tools.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
35 changes: 29 additions & 6 deletions packages/pigeon/lib/src/pigeon_lib.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>] for
/// multiple files.
final Object? swiftOut;

/// Returns all output paths for Swift from [swiftOut].
Iterable<String>? get swiftOutPaths {
final Object? out = swiftOut;
if (out == null) {
return null;
}
if (out is String) {
return <String>[out];
}
if (out is Iterable) {
return out.whereType<String>();
}
return null;
}

/// Options that control how Swift will be generated.
final SwiftOptions? swiftOptions;
Expand Down Expand Up @@ -361,7 +379,9 @@ class PigeonOptions {
javaOptions: map.containsKey('javaOptions')
? JavaOptions.fromMap(map['javaOptions']! as Map<String, Object>)
: null,
swiftOut: map['swiftOut'] as String?,
swiftOut: map['swiftOut'] is Iterable
? (map['swiftOut']! as Iterable<dynamic>).cast<String>().toList()
: map['swiftOut'] as String?,
swiftOptions: map.containsKey('swiftOptions')
? SwiftOptions.fromList(map['swiftOptions']! as Map<String, Object>)
: null,
Expand Down Expand Up @@ -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 <String>['experimental_swift_out'],
)
..addOption(
Expand Down Expand Up @@ -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<String>;

final opts = PigeonOptions(
input: results['input'] as String?,
Expand All @@ -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?,
Expand Down
35 changes: 31 additions & 4 deletions packages/pigeon/lib/src/pigeon_lib_internal.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -405,12 +405,39 @@ class SwiftGeneratorAdapter implements GeneratorAdapter {
return;
}
const generator = SwiftGenerator();
generator.generate(options.swiftOptions!, root, sink, dartPackageName: options.dartPackageName);
final List<String> 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));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using path.posix.join forces POSIX-style forward slashes as path separators. Since this code is performing local file system operations (File(...)), it is better to use platform-agnostic path.join to ensure correct path resolution on Windows and other platforms.

Suggested change
final file = File(path.posix.join(options.basePath ?? '', outputPath));
final file = File(path.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<Error> validate(InternalPigeonOptions options, Root root) {
Expand Down
23 changes: 20 additions & 3 deletions packages/pigeon/lib/src/swift/swift_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class InternalSwiftOptions extends InternalOptions {
const InternalSwiftOptions({
this.copyrightHeader,
required this.swiftOut,
this.swiftOuts,
this.fileSpecificClassNameComponent,
this.errorClassName,
this.includeErrorClass = true,
Expand All @@ -92,22 +93,38 @@ class InternalSwiftOptions extends InternalOptions {
/// Creates InternalSwiftOptions from SwiftOptions.
InternalSwiftOptions.fromSwiftOptions(
SwiftOptions options, {
required this.swiftOut,
Iterable<String>? swiftOuts,
String? swiftOut,
Iterable<String>? copyrightHeader,
}) : copyrightHeader = options.copyrightHeader ?? copyrightHeader,
fileSpecificClassNameComponent =
options.fileSpecificClassNameComponent ??
swiftOut.split('/').lastOrNull?.split('.').firstOrNull ??
(swiftOuts?.firstOrNull ?? swiftOut ?? '')
.split('/')
.lastOrNull
?.split('.')
.firstOrNull ??
'',
Comment on lines +102 to 107

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When running on Windows, file paths typically use backslashes (\\) as separators. Splitting only by / will fail to extract the file name correctly, leading to invalid or colliding class names (e.g., C:\\path\\to\\file.swift would result in C:\\path\\to\\file instead of file). Replacing backslashes with forward slashes before splitting resolves this issue.

Suggested change
(swiftOuts?.firstOrNull ?? swiftOut ?? '')
.split('/')
.lastOrNull
?.split('.')
.firstOrNull ??
'',
(swiftOuts?.firstOrNull ?? swiftOut ?? '')
.replaceAll('\\', '/')
.split('/')
.lastOrNull
?.split('.')
.firstOrNull ??
'',

errorClassName = options.errorClassName,
includeErrorClass = options.includeErrorClass;
includeErrorClass = options.includeErrorClass,
swiftOut = swiftOut ?? swiftOuts?.firstOrNull ?? '',
swiftOuts = swiftOuts ?? (swiftOut != null ? <String>[swiftOut] : const <String>[]);

/// A copyright header that will get prepended to generated code.
final Iterable<String>? 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<String>? swiftOuts;

/// Returns all output paths for Swift.
Iterable<String> get allSwiftOuts =>
swiftOuts ?? (swiftOut.isNotEmpty ? <String>[swiftOut] : const <String>[]);

/// A String to augment class names to avoid cross file collisions.
final String? fileSpecificClassNameComponent;

Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
125 changes: 125 additions & 0 deletions packages/pigeon/test/pigeon_lib_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -95,6 +96,21 @@ void main() {
expect(opts.swiftOut, equals('Foo.swift'));
});

test('parse args - multiple swift_out', () {
final PigeonOptions opts = Pigeon.parseArgs(<String>[
'--swift_out',
'Foo.swift',
'--swift_out',
'Bar.swift',
]);
expect(opts.swiftOut, equals(<String>['Foo.swift', 'Bar.swift']));
});

test('parse args - comma-separated swift_out', () {
final PigeonOptions opts = Pigeon.parseArgs(<String>['--swift_out', 'Foo.swift,Bar.swift']);
expect(opts.swiftOut, equals(<String>['Foo.swift', 'Bar.swift']));
});

test('parse args - kotlin_out', () {
final PigeonOptions opts = Pigeon.parseArgs(<String>['--kotlin_out', 'Foo.kt']);
expect(opts.kotlinOut, equals('Foo.kt'));
Expand Down Expand Up @@ -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, <String>['Foo.swift']);
});

test('@ConfigurePigeon swiftOut multiple', () {
const code = '''
@ConfigurePigeon(PigeonOptions(
swiftOut: <String>['Foo.swift', 'Bar.swift'],
))
class Message {
int? id;
}
''';

final ParseResults results = parseSource(code);
final PigeonOptions options = PigeonOptions.fromMap(results.pigeonOptions!);
expect(options.swiftOut, <String>['Foo.swift', 'Bar.swift']);
expect(options.swiftOutPaths, <String>['Foo.swift', 'Bar.swift']);
});

test('multiple swiftOut generation', () async {
final completer = Completer<void>();
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: <String>[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<void>();
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(<String>[
'--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()
Expand Down
2 changes: 1 addition & 1 deletion packages/pigeon/tool/shared/generation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ Future<int> runPigeon({
bool kotlinIncludeErrorClass = true,
bool kotlinUseGeneratedAnnotation = false,
bool swiftIncludeErrorClass = true,
String? swiftOut,
Object? swiftOut,
String? swiftErrorClassName,
String? cppHeaderOut,
String? cppSourceOut,
Expand Down
Loading