| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| import 'dart:io'; | |
| import 'package:ffigen/ffigen.dart' as fg; | |
| import 'package:logging/logging.dart'; | |
| import 'package:native_test_helpers/native_test_helpers.dart'; | |
| import 'package:path/path.dart' as path; | |
| import 'package:swiftgen/src/util.dart'; | |
| import 'package:swiftgen/swiftgen.dart'; | |
| import 'package:test/test.dart'; | |
| String pkgDir = findPackageRoot('swiftgen').toFilePath(); | |
| Future<Target> hostTarget = Target.host(); | |
| // There are language features that aren't supported in Swift2ObjC yet, but that | |
| // we want to test in SwiftGen. So for now we write @objc annotated bindings for | |
| // these features. As each of these features is supported, migrate the test to | |
| // stop using hand-written @objc annotations. | |
| // TODO(https://github.com/dart-lang/native/issues/1669): callbacks | |
| // TODO(https://github.com/dart-lang/native/issues/1828): protocols | |
| const objCCompatibleTests = {'callbacks', 'protocols'}; | |
| class TestGenerator { | |
| final String name; | |
| late final String testDir; | |
| late final String tempDir; | |
| late final String inputFile; | |
| late final String wrapperFile; | |
| late final String outputFile; | |
| late final String outputObjCFile; | |
| late final String objInputFile; | |
| late final String objWrapperFile; | |
| late final String objObjCFile; | |
| late final String dylibFile; | |
| late final String actualOutputFile; | |
| final bool isObjCCompatible; | |
| TestGenerator(this.name) | |
| : isObjCCompatible = objCCompatibleTests.contains(name) { | |
| testDir = path.absolute(path.join(pkgDir, 'test/integration')); | |
| tempDir = path.join(testDir, 'temp'); | |
| inputFile = path.join(testDir, '$name.swift'); | |
| wrapperFile = path.join(tempDir, '${name}_wrapper.swift'); | |
| outputFile = path.join(tempDir, '${name}_output.dart'); | |
| outputObjCFile = path.join(tempDir, '${name}_output.m'); | |
| objInputFile = path.join(tempDir, '$name.o'); | |
| objWrapperFile = path.join(tempDir, '${name}_wrapper.o'); | |
| objObjCFile = path.join(tempDir, '${name}_output_m.o'); | |
| dylibFile = path.join(tempDir, '$name.dylib'); | |
| actualOutputFile = path.join(testDir, '${name}_bindings.dart'); | |
| } | |
| Future<void> generateBindings([Target? target]) async => | |
| await SwiftGenerator( | |
| target: target ?? await hostTarget, | |
| inputs: [ | |
| isObjCCompatible | |
| ? ObjCCompatibleSwiftFileInput(files: [Uri.file(inputFile)]) | |
| : SwiftFileInput( | |
| files: [Uri.file(inputFile)], | |
| tempModuleName: name, | |
| ), | |
| ], | |
| output: Output( | |
| swiftWrapperFile: isObjCCompatible | |
| ? null | |
| : SwiftWrapperFile(path: Uri.file(wrapperFile)), | |
| module: name, | |
| dartFile: Uri.file(outputFile), | |
| objectiveCFile: Uri.file(outputObjCFile), | |
| preamble: ''' | |
| // Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file | |
| // for details. All rights reserved. Use of this source code is governed by a | |
| // BSD-style license that can be found in the LICENSE file. | |
| // coverage:ignore-file | |
| ''', | |
| ), | |
| ffigen: FfiGeneratorOptions( | |
| visitors: [ | |
| fg.Visitor( | |
| objCInterface: (node) { | |
| if (node.name.startsWith('Test')) { | |
| node.isIncluded = true; | |
| } | |
| }, | |
| objCProtocol: (node) { | |
| if (node.name.startsWith('Test')) { | |
| node.isIncluded = true; | |
| } | |
| }, | |
| ), | |
| ], | |
| ), | |
| ).generate( | |
| logger: Logger.root..level = Level.SEVERE, | |
| tempDirectory: Uri.directory(tempDir), | |
| ); | |
| Future<void> generateAndVerifyBindings([Target? compileTarget]) async { | |
| // Run the generation pipeline. This produces the swift compatability | |
| // wrapper, and the ffigen wrapper. | |
| await generateBindings(compileTarget); | |
| expect(File(inputFile).existsSync(), isTrue); | |
| expect(File(wrapperFile).existsSync(), !isObjCCompatible); | |
| expect(File(outputFile).existsSync(), isTrue); | |
| // The generation pipeline also an obj file as a byproduct. | |
| expect(File(objInputFile).existsSync(), isTrue); | |
| final target = compileTarget ?? await hostTarget; | |
| await run('swiftc', [ | |
| '-c', | |
| inputFile, | |
| if (!isObjCCompatible) wrapperFile, | |
| '-module-name', | |
| name, | |
| '-target', | |
| target.triple, | |
| '-sdk', | |
| path.absolute(target.sdk.toFilePath()), | |
| ], tempDir); | |
| final hasOutputObjCFile = File(outputObjCFile).existsSync(); | |
| if (hasOutputObjCFile) { | |
| // We also need to compile outputObjCFile to an obj file. | |
| await run('clang', [ | |
| '-x', | |
| 'objective-c', | |
| '-fobjc-arc', | |
| '-c', | |
| outputObjCFile, | |
| '-fpic', | |
| '-target', | |
| target.triple, | |
| '-isysroot', | |
| path.absolute(target.sdk.toFilePath()), | |
| '-o', | |
| objObjCFile, | |
| ], tempDir); | |
| expect(File(objObjCFile).existsSync(), isTrue); | |
| } | |
| // Link all the obj files into a dylib. | |
| await run('clang', [ | |
| '-shared', | |
| '-framework', | |
| 'Foundation', | |
| '-target', | |
| target.triple, | |
| '-isysroot', | |
| path.absolute(target.sdk.toFilePath()), | |
| objInputFile, | |
| if (!isObjCCompatible) objWrapperFile, | |
| if (hasOutputObjCFile) objObjCFile, | |
| '-o', | |
| dylibFile, | |
| ], tempDir); | |
| expect(File(dylibFile).existsSync(), isTrue); | |
| // Expect that the bindings match. | |
| expect( | |
| File(outputFile).readAsStringSync(), | |
| File(actualOutputFile).readAsStringSync(), | |
| ); | |
| } | |
| } |