From 7faf62c300c0f60a8f8d3644c010ee09622c4c79 Mon Sep 17 00:00:00 2001 From: Gabriel Terwesten Date: Thu, 14 Sep 2023 12:20:32 +0200 Subject: [PATCH] Add `includes`, `flags`, `std`, `language`, `cppLinkStdLib` options (#125) --- pkgs/native_toolchain_c/CHANGELOG.md | 8 + .../lib/src/cbuilder/cbuilder.dart | 91 ++- .../lib/src/cbuilder/run_cbuilder.dart | 70 +- .../lib/src/native_toolchain/xcode.dart | 4 +- pkgs/native_toolchain_c/pubspec.yaml | 4 +- .../cbuilder/cbuilder_build_failure_test.dart | 75 +-- .../cbuilder/cbuilder_cross_android_test.dart | 94 ++- .../cbuilder/cbuilder_cross_ios_test.dart | 169 +++-- .../cbuilder_cross_linux_host_test.dart | 76 +-- .../cbuilder_cross_macos_host_test.dart | 75 +-- .../cbuilder_cross_windows_host_test.dart | 84 ++- .../test/cbuilder/cbuilder_test.dart | 633 +++++++++++++----- .../test/cbuilder/compiler_resolver_test.dart | 111 ++- .../hello_world_cpp/src/hello_world_cpp.cc | 14 + .../testfiles/includes/include/includes.h | 12 + .../testfiles/includes/src/includes.c | 6 + pkgs/native_toolchain_c/test/helpers.dart | 28 +- .../native_toolchain/recognizer_test.dart | 27 +- .../test/tool/tool_resolver_test.dart | 92 ++- .../test/utils/run_process_test.dart | 17 +- 20 files changed, 1061 insertions(+), 629 deletions(-) create mode 100644 pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc create mode 100644 pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/include/includes.h create mode 100644 pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/src/includes.c diff --git a/pkgs/native_toolchain_c/CHANGELOG.md b/pkgs/native_toolchain_c/CHANGELOG.md index b4b92b4a3..4d2cf753f 100644 --- a/pkgs/native_toolchain_c/CHANGELOG.md +++ b/pkgs/native_toolchain_c/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.2.4 + +- Added `includes` for specifying include directories. +- Added `flags` for specifying arbitrary compiler flags. +- Added `std` for specifying a language standard. +- Added `language` for selecting the language (`c` and `cpp`) to compile source files as. +- Added `cppLinkStdLib` for specifying the C++ standard library to link against. + ## 0.2.3 - Fix MSVC tool resolution inside (x86) folder diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart index ab43adb69..6f689fe0d 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart @@ -18,6 +18,25 @@ abstract class Builder { }); } +/// A programming language that can be selected for compilation of source files. +/// +/// See [CBuilder.language] for more information. +class Language { + /// The name of the language. + final String name; + + const Language._(this.name); + + static const Language c = Language._('c'); + static const Language cpp = Language._('c++'); + + /// Known values for [Language]. + static const List values = [c, cpp]; + + @override + String toString() => name; +} + /// Specification for building an artifact with a C compiler. class CBuilder implements Builder { /// What kind of artifact to build. @@ -45,6 +64,13 @@ class CBuilder implements Builder { /// Used to output the [BuildOutput.dependencies]. final List sources; + /// Include directories to pass to the compiler. + /// + /// Resolved against [BuildConfig.packageRoot]. + /// + /// Used to output the [BuildOutput.dependencies]. + final List includes; + /// The dart files involved in building this artifact. /// /// Resolved against [BuildConfig.packageRoot]. @@ -57,6 +83,9 @@ class CBuilder implements Builder { @visibleForTesting final Uri? installName; + /// Flags to pass to the compiler. + final List flags; + /// Definitions of preprocessor macros. /// /// When the value is `null`, the macro is defined without a value. @@ -95,26 +124,64 @@ class CBuilder implements Builder { /// Defaults to `true` for libraries and `false` for executables. final bool? pic; + /// The language standard to use. + /// + /// When set to `null`, the default behavior of the compiler will be used. + final String? std; + + /// The language to compile [sources] as. + /// + /// [cppLinkStdLib] only has an effect when this option is set to + /// [Langauge.cpp]. + final Language language; + + /// The C++ standard library to link against. + /// + /// This option has no effect when [language] is not set to [Language.cpp] or + /// when compiling for Windows. + /// + /// When set to `null`, the following defaults will be used, based on the + /// target OS: + /// + /// | OS | Library | + /// | :------ | :----------- | + /// | Android | `c++_shared` | + /// | iOS | `c++` | + /// | Linux | `stdc++` | + /// | macOS | `c++` | + /// | Fuchsia | `c++` | + final String? cppLinkStdLib; + CBuilder.library({ required this.name, required this.assetId, this.sources = const [], + this.includes = const [], this.dartBuildFiles = const ['build.dart'], @visibleForTesting this.installName, + this.flags = const [], this.defines = const {}, this.buildModeDefine = true, this.ndebugDefine = true, this.pic = true, + this.std, + this.language = Language.c, + this.cppLinkStdLib, }) : _type = _CBuilderType.library; CBuilder.executable({ required this.name, this.sources = const [], + this.includes = const [], this.dartBuildFiles = const ['build.dart'], + this.flags = const [], this.defines = const {}, this.buildModeDefine = true, this.ndebugDefine = true, bool? pie = false, + this.std, + this.language = Language.c, + this.cppLinkStdLib, }) : _type = _CBuilderType.executable, assetId = null, installName = null, @@ -141,6 +208,10 @@ class CBuilder implements Builder { for (final source in this.sources) packageRoot.resolveUri(Uri.file(source)), ]; + final includes = [ + for (final directory in this.includes) + packageRoot.resolveUri(Uri.file(directory)), + ]; final dartBuildFiles = [ for (final source in this.dartBuildFiles) packageRoot.resolve(source), ]; @@ -149,6 +220,7 @@ class CBuilder implements Builder { buildConfig: buildConfig, logger: logger, sources: sources, + includes: includes, dynamicLibrary: _type == _CBuilderType.library && linkMode == LinkMode.dynamic ? libUri @@ -159,6 +231,7 @@ class CBuilder implements Builder { : null, executable: _type == _CBuilderType.executable ? exeUri : null, installName: installName, + flags: flags, defines: { ...defines, if (buildModeDefine) buildConfig.buildMode.name.toUpperCase(): null, @@ -166,6 +239,9 @@ class CBuilder implements Builder { 'NDEBUG': null, }, pic: pic, + std: std, + language: language, + cppLinkStdLib: cppLinkStdLib, ); await task.run(); } @@ -188,10 +264,21 @@ class CBuilder implements Builder { } } if (!buildConfig.dryRun) { - buildOutput.dependencies.dependencies.addAll([ + final includeFiles = await Stream.fromIterable(includes) + .asyncExpand( + (include) => Directory(include.toFilePath()) + .list(recursive: true) + .where((entry) => entry is File) + .map((file) => file.uri), + ) + .toList(); + + buildOutput.dependencies.dependencies.addAll({ + // Note: We use a Set here to deduplicate the dependencies. ...sources, + ...includeFiles, ...dartBuildFiles, - ]); + }); } } } diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart index 171f89e3c..801504c58 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart @@ -13,12 +13,14 @@ import '../native_toolchain/xcode.dart'; import '../tool/tool_instance.dart'; import '../utils/env_from_bat.dart'; import '../utils/run_process.dart'; +import 'cbuilder.dart'; import 'compiler_resolver.dart'; class RunCBuilder { final BuildConfig buildConfig; final Logger? logger; final List sources; + final List includes; final Uri? executable; final Uri? dynamicLibrary; final Uri? staticLibrary; @@ -32,25 +34,42 @@ class RunCBuilder { /// Can be modified with `install_name_tool`. final Uri? installName; + final List flags; final Map defines; final bool? pic; + final String? std; + final Language language; + final String? cppLinkStdLib; RunCBuilder({ required this.buildConfig, this.logger, this.sources = const [], + this.includes = const [], this.executable, this.dynamicLibrary, this.staticLibrary, this.installName, + this.flags = const [], this.defines = const {}, this.pic, + this.std, + this.language = Language.c, + this.cppLinkStdLib, }) : outDir = buildConfig.outDir, target = buildConfig.target, assert([executable, dynamicLibrary, staticLibrary] .whereType() .length == - 1); + 1) { + if (target.os == OS.windows && cppLinkStdLib != null) { + throw ArgumentError.value( + cppLinkStdLib, + 'cppLinkStdLib', + 'is not supported when targeting Windows', + ); + } + } late final _resolver = CompilerResolver(buildConfig: buildConfig, logger: logger); @@ -133,20 +152,6 @@ class RunCBuilder { '-install_name', installName!.toFilePath(), ], - ...sources.map((e) => e.toFilePath()), - if (executable != null) ...[ - '-o', - outDir.resolveUri(executable!).toFilePath(), - ], - if (dynamicLibrary != null) ...[ - '--shared', - '-o', - outDir.resolveUri(dynamicLibrary!).toFilePath(), - ] else if (staticLibrary != null) ...[ - '-c', - '-o', - outDir.resolve('out.o').toFilePath(), - ], if (pic != null) if (pic!) ...[ if (dynamicLibrary != null) '-fPIC', @@ -165,8 +170,31 @@ class RunCBuilder { 'notext', ] ], + if (std != null) '-std=$std', + if (language == Language.cpp) ...[ + '-x', + 'c++', + '-l', + cppLinkStdLib ?? defaultCppLinkStdLib[target.os]! + ], + ...flags, for (final MapEntry(key: name, :value) in defines.entries) if (value == null) '-D$name' else '-D$name=$value', + for (final include in includes) '-I${include.toFilePath()}', + ...sources.map((e) => e.toFilePath()), + if (executable != null) ...[ + '-o', + outDir.resolveUri(executable!).toFilePath(), + ], + if (dynamicLibrary != null) ...[ + '--shared', + '-o', + outDir.resolveUri(dynamicLibrary!).toFilePath(), + ] else if (staticLibrary != null) ...[ + '-c', + '-o', + outDir.resolve('out.o').toFilePath(), + ], ], logger: logger, captureOutput: false, @@ -201,8 +229,12 @@ class RunCBuilder { final result = await runProcess( executable: compiler.uri, arguments: [ + if (std != null) '/std:$std', + if (language == Language.cpp) '/TP', + ...flags, for (final MapEntry(key: name, :value) in defines.entries) if (value == null) '/D$name' else '/D$name=$value', + for (final directory in includes) '/I${directory.toFilePath()}', if (executable != null) ...[ ...sources.map((e) => e.toFilePath()), '/link', @@ -265,4 +297,12 @@ class RunCBuilder { IOSSdk.iPhoneSimulator: 'x86_64-apple-ios-simulator', }, }; + + static const defaultCppLinkStdLib = { + OS.android: 'c++_shared', + OS.fuchsia: 'c++', + OS.iOS: 'c++', + OS.linux: 'stdc++', + OS.macOS: 'c++', + }; } diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart index 4f239ef41..48c25c3c0 100644 --- a/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart @@ -90,11 +90,11 @@ class XCodeSdkResolver implements ToolResolver { } assert(result.exitCode == 0); final uriSymbolic = Uri.directory(result.stdout.trim()); - logger?.fine('Found $sdk at ${uriSymbolic.toFilePath()}}'); + logger?.fine('Found $sdk at ${uriSymbolic.toFilePath()}'); final uri = Uri.directory( await Directory.fromUri(uriSymbolic).resolveSymbolicLinks()); if (uriSymbolic != uri) { - logger?.fine('Found $sdk at ${uri.toFilePath()}}'); + logger?.fine('Found $sdk at ${uri.toFilePath()}'); } assert(await Directory.fromUri(uri).exists()); return [ToolInstance(tool: tool, uri: uri)]; diff --git a/pkgs/native_toolchain_c/pubspec.yaml b/pkgs/native_toolchain_c/pubspec.yaml index 41b85bc7d..8195f0878 100644 --- a/pkgs/native_toolchain_c/pubspec.yaml +++ b/pkgs/native_toolchain_c/pubspec.yaml @@ -1,7 +1,7 @@ name: native_toolchain_c description: >- A library to invoke the native C compiler installed on the host machine. -version: 0.2.3 +version: 0.2.4 repository: https://github.com/dart-lang/native/tree/main/pkgs/native_toolchain_c topics: @@ -12,7 +12,7 @@ topics: - native-toolchain environment: - sdk: '>=3.0.0 <4.0.0' + sdk: '>=3.1.0 <4.0.0' dependencies: cli_config: ^0.1.1 diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_build_failure_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_build_failure_test.dart index ba65f5a53..af510da3a 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_build_failure_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_build_failure_test.dart @@ -18,47 +18,44 @@ import '../helpers.dart'; void main() { test('build failure', () async { - await inTempDir( - (tempUri) async { - final addCOriginalUri = - packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); - final addCUri = tempUri.resolve('add.c'); - final addCOriginalContents = - await File.fromUri(addCOriginalUri).readAsString(); - final addCBrokenContents = addCOriginalContents.replaceAll( - 'int32_t a, int32_t b', 'int64_t blabla'); - await File.fromUri(addCUri).writeAsString(addCBrokenContents); - const name = 'add'; + final tempUri = await tempDirForTest(); + final addCOriginalUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + final addCUri = tempUri.resolve('add.c'); + final addCOriginalContents = + await File.fromUri(addCOriginalUri).readAsString(); + final addCBrokenContents = addCOriginalContents.replaceAll( + 'int32_t a, int32_t b', 'int64_t blabla'); + await File.fromUri(addCUri).writeAsString(addCBrokenContents); + const name = 'add'; - final buildConfig = BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - targetArchitecture: Architecture.current, - targetOs: OS.current, - linkModePreference: LinkModePreference.dynamic, - buildMode: BuildMode.release, - cCompiler: CCompilerConfig( - cc: cc, - envScript: envScript, - envScriptArgs: envScriptArgs, - ), - ); - final buildOutput = BuildOutput(); + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + linkModePreference: LinkModePreference.dynamic, + buildMode: BuildMode.release, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); - final cbuilder = CBuilder.library( - sources: [addCUri.toFilePath()], - name: name, - assetId: name, - ); - expect( - () => cbuilder.run( - buildConfig: buildConfig, - buildOutput: buildOutput, - logger: logger, - ), - throwsException, - ); - }, + final cbuilder = CBuilder.library( + sources: [addCUri.toFilePath()], + name: name, + assetId: name, + ); + expect( + () => cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ), + throwsException, ); }); } diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart index 276c618da..fb4a794a1 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_android_test.dart @@ -42,37 +42,36 @@ void main() { for (final linkMode in LinkMode.values) { for (final target in targets) { test('CBuilder $linkMode library $target', () async { - await inTempDir((tempUri) async { - final libUri = await buildLib( - tempUri, - target, - flutterAndroidNdkVersionLowestSupported, - linkMode, + final tempUri = await tempDirForTest(); + final libUri = await buildLib( + tempUri, + target, + flutterAndroidNdkVersionLowestSupported, + linkMode, + ); + if (Platform.isLinux) { + final result = await runProcess( + executable: Uri.file('readelf'), + arguments: ['-h', libUri.path], + logger: logger, ); - if (Platform.isLinux) { - final result = await runProcess( - executable: Uri.file('readelf'), - arguments: ['-h', libUri.path], - logger: logger, - ); - expect(result.exitCode, 0); - final machine = result.stdout - .split('\n') - .firstWhere((e) => e.contains('Machine:')); - expect(machine, contains(readElfMachine[target])); - } else if (Platform.isMacOS) { - final result = await runProcess( - executable: Uri.file('objdump'), - arguments: ['-T', libUri.path], - logger: logger, - ); - expect(result.exitCode, 0); - final machine = result.stdout - .split('\n') - .firstWhere((e) => e.contains('file format')); - expect(machine, contains(objdumpFileFormat[target])); - } - }); + expect(result.exitCode, 0); + final machine = result.stdout + .split('\n') + .firstWhere((e) => e.contains('Machine:')); + expect(machine, contains(readElfMachine[target])); + } else if (Platform.isMacOS) { + final result = await runProcess( + executable: Uri.file('objdump'), + arguments: ['-T', libUri.path], + logger: logger, + ); + expect(result.exitCode, 0); + final machine = result.stdout + .split('\n') + .firstWhere((e) => e.contains('file format')); + expect(machine, contains(objdumpFileFormat[target])); + } }); } } @@ -82,24 +81,23 @@ void main() { const linkMode = LinkMode.dynamic; const apiLevel1 = flutterAndroidNdkVersionLowestSupported; const apiLevel2 = flutterAndroidNdkVersionHighestSupported; - await inTempDir((tempUri) async { - final out1Uri = tempUri.resolve('out1/'); - final out2Uri = tempUri.resolve('out2/'); - final out3Uri = tempUri.resolve('out3/'); - await Directory.fromUri(out1Uri).create(); - await Directory.fromUri(out2Uri).create(); - await Directory.fromUri(out3Uri).create(); - final lib1Uri = await buildLib(out1Uri, target, apiLevel1, linkMode); - final lib2Uri = await buildLib(out2Uri, target, apiLevel2, linkMode); - final lib3Uri = await buildLib(out3Uri, target, apiLevel2, linkMode); - final bytes1 = await File.fromUri(lib1Uri).readAsBytes(); - final bytes2 = await File.fromUri(lib2Uri).readAsBytes(); - final bytes3 = await File.fromUri(lib3Uri).readAsBytes(); - // Different API levels should lead to a different binary. - expect(bytes1, isNot(bytes2)); - // Identical API levels should lead to an identical binary. - expect(bytes2, bytes3); - }); + final tempUri = await tempDirForTest(); + final out1Uri = tempUri.resolve('out1/'); + final out2Uri = tempUri.resolve('out2/'); + final out3Uri = tempUri.resolve('out3/'); + await Directory.fromUri(out1Uri).create(); + await Directory.fromUri(out2Uri).create(); + await Directory.fromUri(out3Uri).create(); + final lib1Uri = await buildLib(out1Uri, target, apiLevel1, linkMode); + final lib2Uri = await buildLib(out2Uri, target, apiLevel2, linkMode); + final lib3Uri = await buildLib(out3Uri, target, apiLevel2, linkMode); + final bytes1 = await File.fromUri(lib1Uri).readAsBytes(); + final bytes2 = await File.fromUri(lib2Uri).readAsBytes(); + final bytes3 = await File.fromUri(lib3Uri).readAsBytes(); + // Different API levels should lead to a different binary. + expect(bytes1, isNot(bytes2)); + // Identical API levels should lead to an identical binary. + expect(bytes2, bytes3); }); } diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_ios_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_ios_test.dart index ff013804f..385b09137 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_ios_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_ios_test.dart @@ -53,98 +53,95 @@ void main() { 'CBuilder $linkMode library $targetIOSSdk $target' ' ${installName ?? ''}' .trim(), () async { - await inTempDir((tempUri) async { - final addCUri = - packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); - final buildConfig = BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - targetArchitecture: target.architecture, - targetOs: target.os, - buildMode: BuildMode.release, - linkModePreference: linkMode == LinkMode.dynamic - ? LinkModePreference.dynamic - : LinkModePreference.static, - targetIOSSdk: targetIOSSdk, - ); - final buildOutput = BuildOutput(); + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: target.architecture, + targetOs: target.os, + buildMode: BuildMode.release, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + targetIOSSdk: targetIOSSdk, + ); + final buildOutput = BuildOutput(); - final cbuilder = CBuilder.library( - name: name, - assetId: name, - sources: [addCUri.toFilePath()], - installName: installName, - ); - await cbuilder.run( - buildConfig: buildConfig, - buildOutput: buildOutput, - logger: logger, - ); + final cbuilder = CBuilder.library( + name: name, + assetId: name, + sources: [addCUri.toFilePath()], + installName: installName, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); - final libUri = tempUri.resolve(libName); - final objdumpResult = await runProcess( - executable: Uri.file('objdump'), - arguments: ['-t', libUri.path], - logger: logger, - ); - expect(objdumpResult.exitCode, 0); - final machine = objdumpResult.stdout - .split('\n') - .firstWhere((e) => e.contains('file format')); - expect(machine, contains(objdumpFileFormat[target])); + final libUri = tempUri.resolve(libName); + final objdumpResult = await runProcess( + executable: Uri.file('objdump'), + arguments: ['-t', libUri.path], + logger: logger, + ); + expect(objdumpResult.exitCode, 0); + final machine = objdumpResult.stdout + .split('\n') + .firstWhere((e) => e.contains('file format')); + expect(machine, contains(objdumpFileFormat[target])); - final otoolResult = await runProcess( - executable: Uri.file('otool'), - arguments: ['-l', libUri.path], - logger: logger, - ); - expect(otoolResult.exitCode, 0); - if (targetIOSSdk == IOSSdk.iPhoneOs || target == Target.iOSX64) { - // The x64 simulator behaves as device, presumably because the - // devices are never x64. - expect(otoolResult.stdout, contains('LC_VERSION_MIN_IPHONEOS')); - expect(otoolResult.stdout, isNot(contains('LC_BUILD_VERSION'))); - } else { - expect(otoolResult.stdout, - isNot(contains('LC_VERSION_MIN_IPHONEOS'))); - expect(otoolResult.stdout, contains('LC_BUILD_VERSION')); - final platform = otoolResult.stdout - .split('\n') - .firstWhere((e) => e.contains('platform')); - const platformIosSimulator = 7; - expect(platform, contains(platformIosSimulator.toString())); - } + final otoolResult = await runProcess( + executable: Uri.file('otool'), + arguments: ['-l', libUri.path], + logger: logger, + ); + expect(otoolResult.exitCode, 0); + if (targetIOSSdk == IOSSdk.iPhoneOs || target == Target.iOSX64) { + // The x64 simulator behaves as device, presumably because the + // devices are never x64. + expect(otoolResult.stdout, contains('LC_VERSION_MIN_IPHONEOS')); + expect(otoolResult.stdout, isNot(contains('LC_BUILD_VERSION'))); + } else { + expect(otoolResult.stdout, + isNot(contains('LC_VERSION_MIN_IPHONEOS'))); + expect(otoolResult.stdout, contains('LC_BUILD_VERSION')); + final platform = otoolResult.stdout + .split('\n') + .firstWhere((e) => e.contains('platform')); + const platformIosSimulator = 7; + expect(platform, contains(platformIosSimulator.toString())); + } - if (linkMode == LinkMode.dynamic) { - final libInstallName = + if (linkMode == LinkMode.dynamic) { + final libInstallName = await runOtoolInstallName(libUri, libName); + if (installName == null) { + // If no install path is passed, we have an absolute path. + final tempName = tempUri.pathSegments.lastWhere((e) => e != ''); + final pathEnding = + Uri.directory(tempName).resolve(libName).toFilePath(); + expect(Uri.file(libInstallName).isAbsolute, true); + expect(libInstallName, contains(pathEnding)); + final targetInstallName = + '@executable_path/Frameworks/$libName'; + await runProcess( + executable: Uri.file('install_name_tool'), + arguments: [ + '-id', + targetInstallName, + libUri.toFilePath(), + ], + logger: logger, + ); + final libInstallName2 = await runOtoolInstallName(libUri, libName); - if (installName == null) { - // If no install path is passed, we have an absolute path. - final tempName = - tempUri.pathSegments.lastWhere((e) => e != ''); - final pathEnding = - Uri.directory(tempName).resolve(libName).toFilePath(); - expect(Uri.file(libInstallName).isAbsolute, true); - expect(libInstallName, contains(pathEnding)); - final targetInstallName = - '@executable_path/Frameworks/$libName'; - await runProcess( - executable: Uri.file('install_name_tool'), - arguments: [ - '-id', - targetInstallName, - libUri.toFilePath(), - ], - logger: logger, - ); - final libInstallName2 = - await runOtoolInstallName(libUri, libName); - expect(libInstallName2, targetInstallName); - } else { - expect(libInstallName, installName.toFilePath()); - } + expect(libInstallName2, targetInstallName); + } else { + expect(libInstallName, installName.toFilePath()); } - }); + } }); } } diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_linux_host_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_linux_host_test.dart index 4ac497b12..9d9b1bb62 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_linux_host_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_linux_host_test.dart @@ -37,48 +37,46 @@ void main() { for (final linkMode in LinkMode.values) { for (final target in targets) { test('CBuilder $linkMode library $target', () async { - await inTempDir((tempUri) async { - final addCUri = - packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); - const name = 'add'; + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; - final buildConfig = BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - targetArchitecture: target.architecture, - targetOs: target.os, - buildMode: BuildMode.release, - linkModePreference: linkMode == LinkMode.dynamic - ? LinkModePreference.dynamic - : LinkModePreference.static, - ); - final buildOutput = BuildOutput(); + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: target.architecture, + targetOs: target.os, + buildMode: BuildMode.release, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + ); + final buildOutput = BuildOutput(); - final cbuilder = CBuilder.library( - name: name, - assetId: name, - sources: [addCUri.toFilePath()], - ); - await cbuilder.run( - buildConfig: buildConfig, - buildOutput: buildOutput, - logger: logger, - ); + final cbuilder = CBuilder.library( + name: name, + assetId: name, + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); - final libUri = - tempUri.resolve(target.os.libraryFileName(name, linkMode)); - final result = await runProcess( - executable: Uri.file('readelf'), - arguments: ['-h', libUri.path], - logger: logger, - ); - expect(result.exitCode, 0); - final machine = result.stdout - .split('\n') - .firstWhere((e) => e.contains('Machine:')); - expect(machine, contains(readElfMachine[target])); - expect(result.exitCode, 0); - }); + final libUri = + tempUri.resolve(target.os.libraryFileName(name, linkMode)); + final result = await runProcess( + executable: Uri.file('readelf'), + arguments: ['-h', libUri.path], + logger: logger, + ); + expect(result.exitCode, 0); + final machine = + result.stdout.split('\n').firstWhere((e) => e.contains('Machine:')); + expect(machine, contains(readElfMachine[target])); + expect(result.exitCode, 0); }); } } diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_macos_host_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_macos_host_test.dart index 43f091eec..a749ee954 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_macos_host_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_macos_host_test.dart @@ -37,47 +37,46 @@ void main() { for (final linkMode in LinkMode.values) { for (final target in targets) { test('CBuilder $linkMode library $target', () async { - await inTempDir((tempUri) async { - final addCUri = - packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); - const name = 'add'; + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; - final buildConfig = BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - targetArchitecture: target.architecture, - targetOs: target.os, - buildMode: BuildMode.release, - linkModePreference: linkMode == LinkMode.dynamic - ? LinkModePreference.dynamic - : LinkModePreference.static, - ); - final buildOutput = BuildOutput(); + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: target.architecture, + targetOs: target.os, + buildMode: BuildMode.release, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + ); + final buildOutput = BuildOutput(); - final cbuilder = CBuilder.library( - name: name, - assetId: name, - sources: [addCUri.toFilePath()], - ); - await cbuilder.run( - buildConfig: buildConfig, - buildOutput: buildOutput, - logger: logger, - ); + final cbuilder = CBuilder.library( + name: name, + assetId: name, + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); - final libUri = - tempUri.resolve(target.os.libraryFileName(name, linkMode)); - final result = await runProcess( - executable: Uri.file('objdump'), - arguments: ['-t', libUri.path], - logger: logger, - ); - expect(result.exitCode, 0); - final machine = result.stdout - .split('\n') - .firstWhere((e) => e.contains('file format')); - expect(machine, contains(objdumpFileFormat[target])); - }); + final libUri = + tempUri.resolve(target.os.libraryFileName(name, linkMode)); + final result = await runProcess( + executable: Uri.file('objdump'), + arguments: ['-t', libUri.path], + logger: logger, + ); + expect(result.exitCode, 0); + final machine = result.stdout + .split('\n') + .firstWhere((e) => e.contains('file format')); + expect(machine, contains(objdumpFileFormat[target])); }); } } diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_windows_host_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_windows_host_test.dart index 4d322eb39..619652bfc 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_windows_host_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_cross_windows_host_test.dart @@ -49,52 +49,50 @@ void main() { for (final linkMode in LinkMode.values) { for (final target in targets) { test('CBuilder $linkMode library $target', () async { - await inTempDir((tempUri) async { - final addCUri = - packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); - const name = 'add'; + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; - final buildConfig = BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - targetOs: target.os, - targetArchitecture: target.architecture, - buildMode: BuildMode.release, - linkModePreference: linkMode == LinkMode.dynamic - ? LinkModePreference.dynamic - : LinkModePreference.static, - ); - final buildOutput = BuildOutput(); + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetOs: target.os, + targetArchitecture: target.architecture, + buildMode: BuildMode.release, + linkModePreference: linkMode == LinkMode.dynamic + ? LinkModePreference.dynamic + : LinkModePreference.static, + ); + final buildOutput = BuildOutput(); - final cbuilder = CBuilder.library( - name: name, - assetId: name, - sources: [addCUri.toFilePath()], - ); - await cbuilder.run( - buildConfig: buildConfig, - buildOutput: buildOutput, - logger: logger, - ); + final cbuilder = CBuilder.library( + name: name, + assetId: name, + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); - final libUri = - tempUri.resolve(target.os.libraryFileName(name, linkMode)); - expect(await File.fromUri(libUri).exists(), true); - final result = await runProcess( - executable: dumpbinUri, - arguments: ['/HEADERS', libUri.toFilePath()], - logger: logger, - ); - expect(result.exitCode, 0); - final machine = result.stdout - .split('\n') - .firstWhere((e) => e.contains('machine')); - expect(machine, contains(dumpbinMachine[target])); - final fileType = result.stdout - .split('\n') - .firstWhere((e) => e.contains('File Type')); - expect(fileType, contains(dumpbinFileType[linkMode])); - }); + final libUri = + tempUri.resolve(target.os.libraryFileName(name, linkMode)); + expect(await File.fromUri(libUri).exists(), true); + final result = await runProcess( + executable: dumpbinUri, + arguments: ['/HEADERS', libUri.toFilePath()], + logger: logger, + ); + expect(result.exitCode, 0); + final machine = + result.stdout.split('\n').firstWhere((e) => e.contains('machine')); + expect(machine, contains(dumpbinMachine[target])); + final fileType = result.stdout + .split('\n') + .firstWhere((e) => e.contains('File Type')); + expect(fileType, contains(dumpbinFileType[linkMode])); }); } } diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_test.dart index a0592be7a..48645351f 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_test.dart @@ -19,6 +19,11 @@ import 'package:test/test.dart'; import '../helpers.dart'; void main() { + test('Langauge.toString', () { + expect(Language.c.toString(), 'c'); + expect(Language.cpp.toString(), 'c++'); + }); + for (final pic in [null, true, false]) { final picTag = switch (pic) { null => 'auto_pic', true => 'pic', false => 'no_pic' }; @@ -27,60 +32,132 @@ void main() { final suffix = testSuffix([buildMode, picTag]); test('CBuilder executable$suffix', () async { - await inTempDir((tempUri) async { - final helloWorldCUri = packageUri - .resolve('test/cbuilder/testfiles/hello_world/src/hello_world.c'); - if (!await File.fromUri(helloWorldCUri).exists()) { - throw Exception('Run the test from the root directory.'); - } - const name = 'hello_world'; - - final logMessages = []; - final logger = createCapturingLogger(logMessages); - - final buildConfig = BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - targetArchitecture: Architecture.current, - targetOs: OS.current, - buildMode: buildMode, - // Ignored by executables. - linkModePreference: LinkModePreference.dynamic, - cCompiler: CCompilerConfig( - cc: cc, - envScript: envScript, - envScriptArgs: envScriptArgs, - ), - ); - final buildOutput = BuildOutput(); - final cbuilder = CBuilder.executable( - name: name, - sources: [helloWorldCUri.toFilePath()], - pie: pic, - ); - await cbuilder.run( - buildConfig: buildConfig, - buildOutput: buildOutput, - logger: logger, - ); + final tempUri = await tempDirForTest(); + final helloWorldCUri = packageUri + .resolve('test/cbuilder/testfiles/hello_world/src/hello_world.c'); + if (!await File.fromUri(helloWorldCUri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'hello_world'; - final executableUri = - tempUri.resolve(Target.current.os.executableFileName(name)); - expect(await File.fromUri(executableUri).exists(), true); - final result = await runProcess( - executable: executableUri, - logger: logger, - ); - expect(result.exitCode, 0); - if (buildMode == BuildMode.debug) { - expect(result.stdout.trim(), startsWith('Running in debug mode.')); - } - expect(result.stdout.trim(), endsWith('Hello world.')); + final logMessages = []; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: buildMode, + // Ignored by executables. + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.executable( + name: name, + sources: [helloWorldCUri.toFilePath()], + pie: pic, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess( + executable: executableUri, + logger: logger, + ); + expect(result.exitCode, 0); + if (buildMode == BuildMode.debug) { + expect(result.stdout.trim(), startsWith('Running in debug mode.')); + } + expect(result.stdout.trim(), endsWith('Hello world.')); + + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(helloWorldCUri.toFilePath()), + ); + + switch ((buildConfig.targetOs, pic)) { + case (OS.windows, _) || (_, null): + expect(compilerInvocation, isNot(contains('-fPIC'))); + expect(compilerInvocation, isNot(contains('-fPIE'))); + expect(compilerInvocation, isNot(contains('-fno-PIC'))); + expect(compilerInvocation, isNot(contains('-fno-PIE'))); + case (_, true): + expect(compilerInvocation, contains('-fPIE')); + case (_, false): + expect(compilerInvocation, contains('-fno-PIC')); + expect(compilerInvocation, contains('-fno-PIE')); + } + }); + } + + for (final dryRun in [true, false]) { + final suffix = testSuffix([if (dryRun) 'dry_run', picTag]); + + test('CBuilder dylib$suffix', () async { + final tempUri = await tempDirForTest(); + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final logMessages = []; + final logger = createCapturingLogger(logMessages); + + final buildConfig = dryRun + ? BuildConfig.dryRun( + outDir: tempUri, + packageRoot: tempUri, + targetOs: OS.current, + linkModePreference: LinkModePreference.dynamic, + ) + : BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + sources: [addCUri.toFilePath()], + name: name, + assetId: name, + pic: pic, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final dylibUri = tempUri.resolve(Target.current.os.dylibFileName(name)); + expect(await File.fromUri(dylibUri).exists(), !dryRun); + if (!dryRun) { + final dylib = openDynamicLibraryForTest(dylibUri.toFilePath()); + final add = dylib.lookupFunction('add'); + expect(add(1, 2), 3); final compilerInvocation = logMessages.singleWhere( - (message) => message.contains(helloWorldCUri.toFilePath()), + (message) => message.contains(addCUri.toFilePath()), ); - switch ((buildConfig.targetOs, pic)) { case (OS.windows, _) || (_, null): expect(compilerInvocation, isNot(contains('-fPIC'))); @@ -88,91 +165,12 @@ void main() { expect(compilerInvocation, isNot(contains('-fno-PIC'))); expect(compilerInvocation, isNot(contains('-fno-PIE'))); case (_, true): - expect(compilerInvocation, contains('-fPIE')); + expect(compilerInvocation, contains('-fPIC')); case (_, false): expect(compilerInvocation, contains('-fno-PIC')); expect(compilerInvocation, contains('-fno-PIE')); } - }); - }); - } - - for (final dryRun in [true, false]) { - final suffix = testSuffix([if (dryRun) 'dry_run', picTag]); - - test('CBuilder dylib$suffix', () async { - await inTempDir( - // https://github.com/dart-lang/sdk/issues/40159 - keepTemp: Platform.isWindows, - (tempUri) async { - final addCUri = - packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); - const name = 'add'; - - final logMessages = []; - final logger = createCapturingLogger(logMessages); - - final buildConfig = dryRun - ? BuildConfig.dryRun( - outDir: tempUri, - packageRoot: tempUri, - targetOs: OS.current, - linkModePreference: LinkModePreference.dynamic, - ) - : BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - targetArchitecture: Architecture.current, - targetOs: OS.current, - buildMode: BuildMode.release, - linkModePreference: LinkModePreference.dynamic, - cCompiler: CCompilerConfig( - cc: cc, - envScript: envScript, - envScriptArgs: envScriptArgs, - ), - ); - final buildOutput = BuildOutput(); - - final cbuilder = CBuilder.library( - sources: [addCUri.toFilePath()], - name: name, - assetId: name, - pic: pic, - ); - await cbuilder.run( - buildConfig: buildConfig, - buildOutput: buildOutput, - logger: logger, - ); - - final dylibUri = - tempUri.resolve(Target.current.os.dylibFileName(name)); - expect(await File.fromUri(dylibUri).exists(), !dryRun); - if (!dryRun) { - final dylib = DynamicLibrary.open(dylibUri.toFilePath()); - final add = dylib.lookupFunction('add'); - expect(add(1, 2), 3); - - final compilerInvocation = logMessages.singleWhere( - (message) => message.contains(addCUri.toFilePath()), - ); - switch ((buildConfig.targetOs, pic)) { - case (OS.windows, _) || (_, null): - expect(compilerInvocation, isNot(contains('-fPIC'))); - expect(compilerInvocation, isNot(contains('-fPIE'))); - expect(compilerInvocation, isNot(contains('-fno-PIC'))); - expect(compilerInvocation, isNot(contains('-fno-PIE'))); - case (_, true): - expect(compilerInvocation, contains('-fPIC')); - case (_, false): - expect(compilerInvocation, contains('-fno-PIC')); - expect(compilerInvocation, contains('-fno-PIE')); - } - } - }, - ); + } }); } } @@ -200,15 +198,9 @@ void main() { () => testDefines(customDefineWithValue: value), ); } -} -Future testDefines({ - BuildMode buildMode = BuildMode.debug, - bool buildModeDefine = false, - bool ndebugDefine = false, - bool? customDefineWithValue, -}) async { - await inTempDir((tempUri) async { + test('CBuilder flags', () async { + final tempUri = await tempDirForTest(); final definesCUri = packageUri.resolve('test/cbuilder/testfiles/defines/src/defines.c'); if (!await File.fromUri(definesCUri).exists()) { @@ -216,12 +208,15 @@ Future testDefines({ } const name = 'defines'; + final logMessages = []; + final logger = createCapturingLogger(logMessages); + final buildConfig = BuildConfig( outDir: tempUri, packageRoot: tempUri, targetArchitecture: Architecture.current, targetOs: OS.current, - buildMode: buildMode, + buildMode: BuildMode.release, // Ignored by executables. linkModePreference: LinkModePreference.dynamic, cCompiler: CCompilerConfig( @@ -231,15 +226,16 @@ Future testDefines({ ), ); final buildOutput = BuildOutput(); + + final flag = switch (buildConfig.targetOs) { + OS.windows => '/DFOO=USER_FLAG', + _ => '-DFOO=USER_FLAG', + }; + final cbuilder = CBuilder.executable( name: name, sources: [definesCUri.toFilePath()], - defines: { - if (customDefineWithValue != null) - 'FOO': customDefineWithValue ? 'BAR' : null, - }, - buildModeDefine: buildModeDefine, - ndebugDefine: ndebugDefine, + flags: [flag], ); await cbuilder.run( buildConfig: buildConfig, @@ -255,43 +251,334 @@ Future testDefines({ logger: logger, ); expect(result.exitCode, 0); + expect(result.stdout, contains('Macro FOO is defined: USER_FLAG')); - if (buildModeDefine) { - expect( - result.stdout, - contains('Macro ${buildMode.name.toUpperCase()} is defined: 1'), - ); - } else { - expect( - result.stdout, - contains('Macro ${buildMode.name.toUpperCase()} is undefined.'), - ); + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(definesCUri.toFilePath()), + ); + expect(compilerInvocation, contains(flag)); + }); + + test('CBuilder includes', () async { + final tempUri = await tempDirForTest(); + final includeDirectoryUri = + packageUri.resolve('test/cbuilder/testfiles/includes/include'); + final includesHUri = packageUri + .resolve('test/cbuilder/testfiles/includes/include/includes.h'); + final includesCUri = + packageUri.resolve('test/cbuilder/testfiles/includes/src/includes.c'); + const name = 'includes'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: name, + assetId: name, + includes: [includeDirectoryUri.toFilePath()], + sources: [includesCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + expect(buildOutput.dependencies.dependencies, contains(includesHUri)); + + final dylibUri = tempUri.resolve(Target.current.os.dylibFileName(name)); + final dylib = openDynamicLibraryForTest(dylibUri.toFilePath()); + final x = dylib.lookup('x'); + expect(x.value, 42); + }); + + test('CBuilder std', () async { + final tempUri = await tempDirForTest(); + final addCUri = packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + const std = 'c99'; + + final logMessages = []; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final stdFlag = switch (buildConfig.targetOs) { + OS.windows => '/std:$std', + _ => '-std=$std', + }; + + final cbuilder = CBuilder.library( + sources: [addCUri.toFilePath()], + name: name, + assetId: name, + std: std, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final dylibUri = tempUri.resolve(Target.current.os.dylibFileName(name)); + + final dylib = openDynamicLibraryForTest(dylibUri.toFilePath()); + final add = dylib.lookupFunction('add'); + expect(add(1, 2), 3); + + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(addCUri.toFilePath()), + ); + expect(compilerInvocation, contains(stdFlag)); + }); + + test('CBuilder compile c++', () async { + final tempUri = await tempDirForTest(); + final helloWorldCppUri = packageUri.resolve( + 'test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc'); + if (!await File.fromUri(helloWorldCppUri).exists()) { + throw Exception('Run the test from the root directory.'); } + const name = 'hello_world_cpp'; - if (ndebugDefine && buildMode != BuildMode.debug) { - expect( - result.stdout, - contains('Macro NDEBUG is defined: 1'), - ); - } else { - expect( - result.stdout, - contains('Macro NDEBUG is undefined.'), + final logMessages = []; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig( + buildMode: BuildMode.release, + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + // Ignored by executables. + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + + final defaultStdLibLinkFlag = switch (buildConfig.targetOs) { + OS.windows => null, + OS.linux => '-l stdc++', + OS.macOS => '-l c++', + _ => throw UnimplementedError(), + }; + + final cbuilder = CBuilder.executable( + name: name, + sources: [helloWorldCppUri.toFilePath()], + language: Language.cpp, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess( + executable: executableUri, + logger: logger, + ); + expect(result.exitCode, 0); + expect(result.stdout.trim(), endsWith('Hello world.')); + + if (defaultStdLibLinkFlag != null) { + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(helloWorldCppUri.toFilePath()), ); + expect(compilerInvocation, contains(defaultStdLibLinkFlag)); } + }); - if (customDefineWithValue != null) { - expect( - result.stdout, - contains( - 'Macro FOO is defined: ${customDefineWithValue ? 'BAR' : '1'}', + test('CBuilder cppLinkStdLib', () async { + final tempUri = await tempDirForTest(); + final helloWorldCppUri = packageUri.resolve( + 'test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc'); + if (!await File.fromUri(helloWorldCppUri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'hello_world_cpp'; + + final logMessages = []; + final logger = createCapturingLogger(logMessages); + + final buildConfig = BuildConfig( + buildMode: BuildMode.release, + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + // Ignored by executables. + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.executable( + name: name, + sources: [helloWorldCppUri.toFilePath()], + language: Language.cpp, + cppLinkStdLib: 'stdc++', + ); + + if (buildConfig.targetOs == OS.windows) { + await expectLater( + () => cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, ), + throwsArgumentError, ); } else { - expect( - result.stdout, - contains('Macro FOO is undefined.'), + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess( + executable: executableUri, + logger: logger, + ); + expect(result.exitCode, 0); + expect(result.stdout.trim(), endsWith('Hello world.')); + + final compilerInvocation = logMessages.singleWhere( + (message) => message.contains(helloWorldCppUri.toFilePath()), ); + expect(compilerInvocation, contains('-l stdc++')); } }); } + +Future testDefines({ + BuildMode buildMode = BuildMode.debug, + bool buildModeDefine = false, + bool ndebugDefine = false, + bool? customDefineWithValue, +}) async { + final tempUri = await tempDirForTest(); + final definesCUri = + packageUri.resolve('test/cbuilder/testfiles/defines/src/defines.c'); + if (!await File.fromUri(definesCUri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'defines'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: buildMode, + // Ignored by executables. + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + cc: cc, + envScript: envScript, + envScriptArgs: envScriptArgs, + ), + ); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.executable( + name: name, + sources: [definesCUri.toFilePath()], + defines: { + if (customDefineWithValue != null) + 'FOO': customDefineWithValue ? 'BAR' : null, + }, + buildModeDefine: buildModeDefine, + ndebugDefine: ndebugDefine, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess( + executable: executableUri, + logger: logger, + ); + expect(result.exitCode, 0); + + if (buildModeDefine) { + expect( + result.stdout, + contains('Macro ${buildMode.name.toUpperCase()} is defined: 1'), + ); + } else { + expect( + result.stdout, + contains('Macro ${buildMode.name.toUpperCase()} is undefined.'), + ); + } + + if (ndebugDefine && buildMode != BuildMode.debug) { + expect( + result.stdout, + contains('Macro NDEBUG is defined: 1'), + ); + } else { + expect( + result.stdout, + contains('Macro NDEBUG is undefined.'), + ); + } + + if (customDefineWithValue != null) { + expect( + result.stdout, + contains( + 'Macro FOO is defined: ${customDefineWithValue ? 'BAR' : '1'}', + ), + ); + } else { + expect( + result.stdout, + contains('Macro FOO is undefined.'), + ); + } +} diff --git a/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart b/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart index 64b6465d3..be7cf5394 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart @@ -21,65 +21,62 @@ import '../helpers.dart'; void main() { test('Config provided compiler', () async { - await inTempDir((tempUri) async { - final ar = [ - ...await appleAr.defaultResolver!.resolve(logger: logger), - ...await lib.defaultResolver!.resolve(logger: logger), - ...await llvmAr.defaultResolver!.resolve(logger: logger), - ].first.uri; - final cc = [ - ...await appleClang.defaultResolver!.resolve(logger: logger), - ...await cl.defaultResolver!.resolve(logger: logger), - ...await clang.defaultResolver!.resolve(logger: logger), - ].first.uri; - final ld = [ - ...await appleLd.defaultResolver!.resolve(logger: logger), - ...await lib.defaultResolver!.resolve(logger: logger), - ...await lld.defaultResolver!.resolve(logger: logger), - ].first.uri; - final envScript = [ - ...await vcvars64.defaultResolver!.resolve(logger: logger) - ].firstOrNull?.uri; - final buildConfig = BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - targetArchitecture: Architecture.current, - targetOs: OS.current, - buildMode: BuildMode.release, - linkModePreference: LinkModePreference.dynamic, - cCompiler: CCompilerConfig( - ar: ar, - cc: cc, - ld: ld, - envScript: envScript, - ), - ); - final resolver = - CompilerResolver(buildConfig: buildConfig, logger: logger); - final compiler = await resolver.resolveCompiler(); - final archiver = await resolver.resolveArchiver(); - expect(compiler.uri, buildConfig.cCompiler.cc); - expect(archiver.uri, buildConfig.cCompiler.ar); - }); + final tempUri = await tempDirForTest(); + final ar = [ + ...await appleAr.defaultResolver!.resolve(logger: logger), + ...await lib.defaultResolver!.resolve(logger: logger), + ...await llvmAr.defaultResolver!.resolve(logger: logger), + ].first.uri; + final cc = [ + ...await appleClang.defaultResolver!.resolve(logger: logger), + ...await cl.defaultResolver!.resolve(logger: logger), + ...await clang.defaultResolver!.resolve(logger: logger), + ].first.uri; + final ld = [ + ...await appleLd.defaultResolver!.resolve(logger: logger), + ...await lib.defaultResolver!.resolve(logger: logger), + ...await lld.defaultResolver!.resolve(logger: logger), + ].first.uri; + final envScript = [ + ...await vcvars64.defaultResolver!.resolve(logger: logger) + ].firstOrNull?.uri; + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.current, + targetOs: OS.current, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + ar: ar, + cc: cc, + ld: ld, + envScript: envScript, + ), + ); + final resolver = CompilerResolver(buildConfig: buildConfig, logger: logger); + final compiler = await resolver.resolveCompiler(); + final archiver = await resolver.resolveArchiver(); + expect(compiler.uri, buildConfig.cCompiler.cc); + expect(archiver.uri, buildConfig.cCompiler.ar); }); test('No compiler found', () async { - await inTempDir((tempUri) async { - final buildConfig = BuildConfig( - outDir: tempUri, - packageRoot: tempUri, - targetArchitecture: Architecture.arm64, - targetOs: OS.windows, - buildMode: BuildMode.release, - linkModePreference: LinkModePreference.dynamic, - ); - final resolver = CompilerResolver( - buildConfig: buildConfig, - logger: logger, - host: Target.androidArm64, // This is never a host. - ); - expect(resolver.resolveCompiler, throwsA(isA())); - expect(resolver.resolveArchiver, throwsA(isA())); - }); + final tempUri = await tempDirForTest(); + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + targetArchitecture: Architecture.arm64, + targetOs: OS.windows, + buildMode: BuildMode.release, + linkModePreference: LinkModePreference.dynamic, + ); + final resolver = CompilerResolver( + buildConfig: buildConfig, + logger: logger, + host: Target.androidArm64, // This is never a host. + ); + expect(resolver.resolveCompiler, throwsA(isA())); + expect(resolver.resolveArchiver, throwsA(isA())); }); } diff --git a/pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc b/pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc new file mode 100644 index 000000000..d883a323c --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/testfiles/hello_world_cpp/src/hello_world_cpp.cc @@ -0,0 +1,14 @@ +// Copyright (c) 2023, 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. + +#include + +int main() { +#ifdef DEBUG + std::cout << "Running in debug mode." << std::endl; +#endif + std::cout << "Hello world." << std::endl; + return 0; +} + diff --git a/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/include/includes.h b/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/include/includes.h new file mode 100644 index 000000000..c2f78a8a1 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/include/includes.h @@ -0,0 +1,12 @@ +// Copyright (c) 2023, 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. + +#if _WIN32 +#define FFI_EXPORT __declspec(dllexport) +#else +#define FFI_EXPORT +#endif + +FFI_EXPORT int x = 42; + diff --git a/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/src/includes.c b/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/src/includes.c new file mode 100644 index 000000000..851907f2a --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/testfiles/includes/src/includes.c @@ -0,0 +1,6 @@ +// Copyright (c) 2023, 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. + +#include "includes.h" + diff --git a/pkgs/native_toolchain_c/test/helpers.dart b/pkgs/native_toolchain_c/test/helpers.dart index c43bf93a1..ab58f0732 100644 --- a/pkgs/native_toolchain_c/test/helpers.dart +++ b/pkgs/native_toolchain_c/test/helpers.dart @@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:async'; +import 'dart:ffi'; import 'dart:io'; import 'package:logging/logging.dart'; @@ -39,24 +40,17 @@ String testSuffix(List tags) => switch (tags) { const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; -Future inTempDir( - Future Function(Uri tempUri) fun, { - String? prefix, - bool keepTemp = false, -}) async { +Future tempDirForTest({String? prefix, bool keepTemp = false}) async { final tempDir = await Directory.systemTemp.createTemp(prefix); // Deal with Windows temp folder aliases. final tempUri = Directory(await tempDir.resolveSymbolicLinks()).uri.normalizePath(); - try { - await fun(tempUri); - } finally { - if ((!Platform.environment.containsKey(keepTempKey) || - Platform.environment[keepTempKey]!.isEmpty) && - !keepTemp) { - await tempDir.delete(recursive: true); - } + if ((!Platform.environment.containsKey(keepTempKey) || + Platform.environment[keepTempKey]!.isEmpty) && + !keepTemp) { + addTearDown(() => tempDir.delete(recursive: true)); } + return tempUri; } /// Logger that outputs the full trace when a test fails. @@ -170,3 +164,11 @@ Future runOtoolInstallName(Uri libraryUri, String libraryName) async { .split(' ')[1]; return installName; } + +/// Opens the [DynamicLibrary] at [path] and register a tear down hook to close +/// it when the current test is done. +DynamicLibrary openDynamicLibraryForTest(String path) { + final library = DynamicLibrary.open(path); + addTearDown(library.close); + return library; +} diff --git a/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart index ecd8f0af6..40112d595 100644 --- a/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart +++ b/pkgs/native_toolchain_c/test/native_toolchain/recognizer_test.dart @@ -50,27 +50,24 @@ void main() async { } test('compiler does not exist', () async { - await inTempDir((tempUri) async { - final recognizer = CompilerRecognizer(tempUri.resolve('asdf')); - final result = await recognizer.resolve(logger: logger); - expect(result, []); - }); + final tempUri = await tempDirForTest(); + final recognizer = CompilerRecognizer(tempUri.resolve('asdf')); + final result = await recognizer.resolve(logger: logger); + expect(result, []); }); test('linker does not exist', () async { - await inTempDir((tempUri) async { - final recognizer = LinkerRecognizer(tempUri.resolve('asdf')); - final result = await recognizer.resolve(logger: logger); - expect(result, []); - }); + final tempUri = await tempDirForTest(); + final recognizer = LinkerRecognizer(tempUri.resolve('asdf')); + final result = await recognizer.resolve(logger: logger); + expect(result, []); }); test('archiver does not exist', () async { - await inTempDir((tempUri) async { - final recognizer = ArchiverRecognizer(tempUri.resolve('asdf')); - final result = await recognizer.resolve(logger: logger); - expect(result, []); - }); + final tempUri = await tempDirForTest(); + final recognizer = ArchiverRecognizer(tempUri.resolve('asdf')); + final result = await recognizer.resolve(logger: logger); + expect(result, []); }); } diff --git a/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart b/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart index 9d88c1830..9f30ce08b 100644 --- a/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart +++ b/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart @@ -58,56 +58,52 @@ void main() { }); test('RelativeToolResolver', () async { - await inTempDir((tempUri) async { - final barExeUri = - tempUri.resolve(Target.current.os.executableFileName('bar')); - final bazExeName = Target.current.os.executableFileName('baz'); - final bazExeUri = tempUri.resolve(bazExeName); - await File.fromUri(barExeUri).writeAsString('dummy'); - await File.fromUri(bazExeUri).writeAsString('dummy'); - expect(await File.fromUri(barExeUri).exists(), true); - expect(await File.fromUri(bazExeUri).exists(), true); - final barResolver = InstallLocationResolver( - toolName: 'bar', - paths: [barExeUri.toFilePath().replaceAll('\\', '/')], - ); - final bazResolver = RelativeToolResolver( - toolName: 'baz', - wrappedResolver: barResolver, - relativePath: Uri.file(bazExeName), - ); - final resolvedBarInstances = await barResolver.resolve(logger: logger); - expect( - resolvedBarInstances, - [ToolInstance(tool: Tool(name: 'bar'), uri: barExeUri)], - ); - final resolvedBazInstances = await bazResolver.resolve(logger: logger); - expect( - resolvedBazInstances, - [ToolInstance(tool: Tool(name: 'baz'), uri: bazExeUri)], - ); - }); + final tempUri = await tempDirForTest(); + final barExeUri = + tempUri.resolve(Target.current.os.executableFileName('bar')); + final bazExeName = Target.current.os.executableFileName('baz'); + final bazExeUri = tempUri.resolve(bazExeName); + await File.fromUri(barExeUri).writeAsString('dummy'); + await File.fromUri(bazExeUri).writeAsString('dummy'); + expect(await File.fromUri(barExeUri).exists(), true); + expect(await File.fromUri(bazExeUri).exists(), true); + final barResolver = InstallLocationResolver( + toolName: 'bar', + paths: [barExeUri.toFilePath().replaceAll('\\', '/')], + ); + final bazResolver = RelativeToolResolver( + toolName: 'baz', + wrappedResolver: barResolver, + relativePath: Uri.file(bazExeName), + ); + final resolvedBarInstances = await barResolver.resolve(logger: logger); + expect( + resolvedBarInstances, + [ToolInstance(tool: Tool(name: 'bar'), uri: barExeUri)], + ); + final resolvedBazInstances = await bazResolver.resolve(logger: logger); + expect( + resolvedBazInstances, + [ToolInstance(tool: Tool(name: 'baz'), uri: bazExeUri)], + ); }); test('logger', () async { - await inTempDir((tempUri) async { - final barExeUri = - tempUri.resolve(Target.current.os.executableFileName('bar')); - final bazExeName = Target.current.os.executableFileName('baz'); - final bazExeUri = tempUri.resolve(bazExeName); - await File.fromUri(barExeUri).writeAsString('dummy'); - final barResolver = InstallLocationResolver( - toolName: 'bar', - paths: [barExeUri.toFilePath().replaceAll('\\', '/')]); - final bazResolver = InstallLocationResolver( - toolName: 'baz', - paths: [bazExeUri.toFilePath().replaceAll('\\', '/')]); - final barLogs = []; - final bazLogs = []; - await barResolver.resolve(logger: createCapturingLogger(barLogs)); - await bazResolver.resolve(logger: createCapturingLogger(bazLogs)); - expect(barLogs.join('\n'), contains('Found [ToolInstance(bar')); - expect(bazLogs.join('\n'), contains('Found no baz')); - }); + final tempUri = await tempDirForTest(); + final barExeUri = + tempUri.resolve(Target.current.os.executableFileName('bar')); + final bazExeName = Target.current.os.executableFileName('baz'); + final bazExeUri = tempUri.resolve(bazExeName); + await File.fromUri(barExeUri).writeAsString('dummy'); + final barResolver = InstallLocationResolver( + toolName: 'bar', paths: [barExeUri.toFilePath().replaceAll('\\', '/')]); + final bazResolver = InstallLocationResolver( + toolName: 'baz', paths: [bazExeUri.toFilePath().replaceAll('\\', '/')]); + final barLogs = []; + final bazLogs = []; + await barResolver.resolve(logger: createCapturingLogger(barLogs)); + await bazResolver.resolve(logger: createCapturingLogger(bazLogs)); + expect(barLogs.join('\n'), contains('Found [ToolInstance(bar')); + expect(bazLogs.join('\n'), contains('Found no baz')); }); } diff --git a/pkgs/native_toolchain_c/test/utils/run_process_test.dart b/pkgs/native_toolchain_c/test/utils/run_process_test.dart index 9e51afae2..21d7c9df8 100644 --- a/pkgs/native_toolchain_c/test/utils/run_process_test.dart +++ b/pkgs/native_toolchain_c/test/utils/run_process_test.dart @@ -13,15 +13,14 @@ void main() { final whichUri = Uri.file(Platform.isWindows ? 'where' : 'which'); test('log contains working dir', () async { - await inTempDir((tempUri) async { - final messages = []; - await runProcess( - executable: whichUri, - workingDirectory: tempUri, - logger: createCapturingLogger(messages), - ); - expect(messages.join('\n'), contains('cd')); - }); + final tempUri = await tempDirForTest(); + final messages = []; + await runProcess( + executable: whichUri, + workingDirectory: tempUri, + logger: createCapturingLogger(messages), + ); + expect(messages.join('\n'), contains('cd')); }); test('log contains env', () async {