blob: f2dd2578adb017878252067ffc975ee82412226c [file] [log] [blame] [edit]
// 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.
@OnPlatform({'mac-os': Timeout.factor(2), 'windows': Timeout.factor(10)})
library;
import 'dart:ffi';
import 'dart:io';
import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:native_toolchain_c/src/utils/run_process.dart';
import 'package:test/test.dart';
import '../helpers.dart';
void main() {
test('Language.toString', () {
expect(Language.c.toString(), 'c');
expect(Language.cpp.toString(), 'c++');
});
final targetOS = OS.current;
final macOSConfig = targetOS == OS.macOS
? MacOSCodeConfig(targetVersion: defaultMacOSVersion)
: null;
for (final pic in [null, true, false]) {
final picTag = switch (pic) {
null => 'auto_pic',
true => 'pic',
false => 'no_pic',
};
for (final buildMode in BuildMode.values) {
final suffix = testSuffix([buildMode, picTag]);
test('CBuilder executable$suffix', () async {
final tempUri = await tempDirForTest();
final tempUri2 = 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 logMessages = <String>[];
final logger = createCapturingLogger(logMessages);
final buildInputBuilder = BuildInputBuilder()
..setupShared(
packageName: name,
packageRoot: tempUri,
outputFile: tempUri.resolve('output.json'),
outputDirectoryShared: tempUri2,
)
..config.setupBuild(linkingEnabled: false)
..addExtension(
CodeAssetExtension(
targetOS: targetOS,
macOS: macOSConfig,
targetArchitecture: Architecture.current,
// Ignored by executables.
linkModePreference: LinkModePreference.dynamic,
cCompiler: cCompiler,
),
);
final buildInput = buildInputBuilder.build();
final buildOutput = BuildOutputBuilder();
final cbuilder = CBuilder.executable(
name: name,
sources: [helloWorldCUri.toFilePath()],
pie: pic,
buildMode: buildMode,
);
await cbuilder.run(
input: buildInput,
output: buildOutput,
logger: logger,
);
final executableUri = buildInput.outputDirectory.resolve(
OS.current.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 ((buildInput.config.code.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 buildCodeAssets in [true, false]) {
final suffix = testSuffix([picTag]);
test('CBuilder dylib$suffix', () async {
final tempUri = await tempDirForTest();
final tempUri2 = await tempDirForTest();
final addCUri = packageUri.resolve(
'test/cbuilder/testfiles/add/src/add.c',
);
const name = 'add';
final logMessages = <String>[];
final logger = createCapturingLogger(logMessages);
final buildInputBuilder = BuildInputBuilder()
..setupShared(
packageName: name,
packageRoot: tempUri,
outputFile: tempUri.resolve('output.json'),
outputDirectoryShared: tempUri2,
)
..config.setupBuild(linkingEnabled: false);
if (buildCodeAssets) {
buildInputBuilder.addExtension(
CodeAssetExtension(
targetOS: targetOS,
macOS: macOSConfig,
targetArchitecture: Architecture.current,
linkModePreference: LinkModePreference.dynamic,
cCompiler: cCompiler,
),
);
}
final buildInput = buildInputBuilder.build();
final buildOutput = BuildOutputBuilder();
final cbuilder = CBuilder.library(
sources: [addCUri.toFilePath()],
name: name,
assetName: name,
pic: pic,
buildMode: BuildMode.release,
);
await cbuilder.run(
input: buildInput,
output: buildOutput,
logger: logger,
);
final dylibUri = buildInput.outputDirectory.resolve(
OS.current.dylibFileName(name),
);
expect(await File.fromUri(dylibUri).exists(), equals(buildCodeAssets));
if (buildCodeAssets) {
final dylib = openDynamicLibraryForTest(dylibUri.toFilePath());
final add = dylib
.lookupFunction<
Int32 Function(Int32, Int32),
int Function(int, int)
>('add');
expect(add(1, 2), 3);
final compilerInvocation = logMessages.singleWhere(
(message) => message.contains(addCUri.toFilePath()),
);
switch ((buildInput.config.code.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'));
}
}
});
}
}
for (final buildMode in BuildMode.values) {
for (final enabled in [true, false]) {
final suffix = testSuffix([buildMode, enabled ? 'enabled' : 'disabled']);
test(
'CBuilder build mode defines$suffix',
() => testDefines(
buildMode: buildMode,
buildModeDefine: enabled,
ndebugDefine: enabled,
),
);
}
}
for (final value in [true, false]) {
final suffix = testSuffix([value ? 'with_value' : 'without_value']);
test(
'CBuilder define$suffix',
() => testDefines(customDefineWithValue: value),
);
}
test('CBuilder flags', () async {
final tempUri = await tempDirForTest();
final tempUri2 = 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.');
}
final forcedIncludeCUri = packageUri.resolve(
'test/cbuilder/testfiles/defines/src/forcedInclude.c',
);
const name = 'defines';
final logMessages = <String>[];
final logger = createCapturingLogger(logMessages);
final buildInputBuilder = BuildInputBuilder()
..setupShared(
packageName: name,
packageRoot: tempUri,
outputFile: tempUri.resolve('output.json'),
outputDirectoryShared: tempUri2,
)
..config.setupBuild(linkingEnabled: false)
..addExtension(
CodeAssetExtension(
targetOS: targetOS,
macOS: macOSConfig,
targetArchitecture: Architecture.current,
// Ignored by executables.
linkModePreference: LinkModePreference.dynamic,
cCompiler: cCompiler,
),
);
final buildInput = buildInputBuilder.build();
final buildOutput = BuildOutputBuilder();
final flag = switch (buildInput.config.code.targetOS) {
OS.windows => '/DFOO=USER_FLAG',
_ => '-DFOO=USER_FLAG',
};
final cbuilder = CBuilder.executable(
name: name,
sources: [definesCUri.toFilePath()],
forcedIncludes: [forcedIncludeCUri.toFilePath()],
flags: [flag],
buildMode: BuildMode.release,
);
await cbuilder.run(input: buildInput, output: buildOutput, logger: logger);
final executableUri = buildInput.outputDirectory.resolve(
OS.current.executableFileName(name),
);
expect(await File.fromUri(executableUri).exists(), true);
final result = await runProcess(executable: executableUri, logger: logger);
expect(result.exitCode, 0);
expect(result.stdout, contains('Macro FOO is defined: USER_FLAG'));
// Check the forced include is added.
expect(result.stdout, contains('Macro FIFOO is defined: "QuotedFIFOO"'));
final compilerInvocation = logMessages.singleWhere(
(message) => message.contains(definesCUri.toFilePath()),
);
expect(compilerInvocation, contains(flag));
});
test('CBuilder includes', () async {
final tempUri = await tempDirForTest();
final tempUri2 = 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 buildInputBuilder = BuildInputBuilder()
..setupShared(
packageName: name,
packageRoot: tempUri,
outputFile: tempUri.resolve('output.json'),
outputDirectoryShared: tempUri2,
)
..config.setupBuild(linkingEnabled: false)
..addExtension(
CodeAssetExtension(
targetOS: targetOS,
macOS: macOSConfig,
targetArchitecture: Architecture.current,
// Ignored by executables.
linkModePreference: LinkModePreference.dynamic,
cCompiler: cCompiler,
),
);
final buildInput = buildInputBuilder.build();
final buildOutputBuilder = BuildOutputBuilder();
final cbuilder = CBuilder.library(
name: name,
assetName: name,
includes: [includeDirectoryUri.toFilePath()],
sources: [includesCUri.toFilePath()],
buildMode: BuildMode.release,
);
await cbuilder.run(
input: buildInput,
output: buildOutputBuilder,
logger: logger,
);
final buildOutput = buildOutputBuilder.build();
expect(buildOutput.dependencies, contains(includesHUri));
final dylibUri = buildInput.outputDirectory.resolve(
OS.current.dylibFileName(name),
);
final dylib = openDynamicLibraryForTest(dylibUri.toFilePath());
final x = dylib.lookup<Int>('x');
expect(x.value, 42);
});
test('CBuilder std', () async {
final tempUri = await tempDirForTest();
final tempUri2 = await tempDirForTest();
final addCUri = packageUri.resolve('test/cbuilder/testfiles/add/src/add.c');
const name = 'add';
const std = 'c99';
final logMessages = <String>[];
final logger = createCapturingLogger(logMessages);
final targetOS = OS.current;
final buildInputBuilder = BuildInputBuilder()
..setupShared(
packageName: name,
packageRoot: tempUri,
outputFile: tempUri.resolve('output.json'),
outputDirectoryShared: tempUri2,
)
..config.setupBuild(linkingEnabled: false)
..addExtension(
CodeAssetExtension(
targetOS: targetOS,
macOS: macOSConfig,
targetArchitecture: Architecture.current,
// Ignored by executables.
linkModePreference: LinkModePreference.dynamic,
cCompiler: cCompiler,
),
);
final buildInput = buildInputBuilder.build();
final buildOutput = BuildOutputBuilder();
final stdFlag = switch (buildInput.config.code.targetOS) {
OS.windows => '/std:$std',
_ => '-std=$std',
};
final cbuilder = CBuilder.library(
sources: [addCUri.toFilePath()],
name: name,
assetName: name,
std: std,
buildMode: BuildMode.release,
);
await cbuilder.run(input: buildInput, output: buildOutput, logger: logger);
final dylibUri = buildInput.outputDirectory.resolve(
OS.current.dylibFileName(name),
);
final dylib = openDynamicLibraryForTest(dylibUri.toFilePath());
final add = dylib
.lookupFunction<Int32 Function(Int32, Int32), int Function(int, int)>(
'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 tempUri2 = 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 = <String>[];
final logger = createCapturingLogger(logMessages);
final targetOS = OS.current;
final buildInputBuilder = BuildInputBuilder()
..setupShared(
packageName: name,
packageRoot: tempUri,
outputFile: tempUri.resolve('output.json'),
outputDirectoryShared: tempUri2,
)
..config.setupBuild(linkingEnabled: false)
..addExtension(
CodeAssetExtension(
targetOS: targetOS,
macOS: macOSConfig,
targetArchitecture: Architecture.current,
// Ignored by executables.
linkModePreference: LinkModePreference.dynamic,
cCompiler: cCompiler,
),
);
final buildInput = buildInputBuilder.build();
final buildOutput = BuildOutputBuilder();
final defaultStdLibLinkFlag = switch (buildInput.config.code.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,
buildMode: BuildMode.release,
);
await cbuilder.run(input: buildInput, output: buildOutput, logger: logger);
final executableUri = buildInput.outputDirectory.resolve(
OS.current.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));
}
});
test('CBuilder cppLinkStdLib', () async {
final tempUri = await tempDirForTest();
final tempUri2 = 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 = <String>[];
final logger = createCapturingLogger(logMessages);
final targetOS = OS.current;
final buildInputBuilder = BuildInputBuilder()
..setupShared(
packageName: name,
packageRoot: tempUri,
outputFile: tempUri.resolve('output.json'),
outputDirectoryShared: tempUri2,
)
..config.setupBuild(linkingEnabled: false)
..addExtension(
CodeAssetExtension(
targetOS: targetOS,
macOS: macOSConfig,
targetArchitecture: Architecture.current,
// Ignored by executables.
linkModePreference: LinkModePreference.dynamic,
cCompiler: cCompiler,
),
);
final buildInput = buildInputBuilder.build();
final buildOutput = BuildOutputBuilder();
final cbuilder = CBuilder.executable(
name: name,
sources: [helloWorldCppUri.toFilePath()],
language: Language.cpp,
cppLinkStdLib: 'stdc++',
buildMode: BuildMode.release,
);
if (buildInput.config.code.targetOS == OS.windows) {
await expectLater(
() => cbuilder.run(
input: buildInput,
output: buildOutput,
logger: logger,
),
throwsArgumentError,
);
} else {
await cbuilder.run(
input: buildInput,
output: buildOutput,
logger: logger,
);
final executableUri = buildInput.outputDirectory.resolve(
OS.current.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++'));
}
});
test('CBuilder libraries and libraryDirectories', () async {
final tempUri = await tempDirForTest();
final tempUri2 = await tempDirForTest();
final dynamicallyLinkedSrcUri = packageUri.resolve(
'test/cbuilder/testfiles/dynamically_linked/src/',
);
final dynamicallyLinkedCUri = dynamicallyLinkedSrcUri.resolve(
'dynamically_linked.c',
);
final debugCUri = dynamicallyLinkedSrcUri.resolve('debug.c');
final mathCUri = dynamicallyLinkedSrcUri.resolve('math.c');
if (!await File.fromUri(dynamicallyLinkedCUri).exists()) {
throw Exception('Run the test from the root directory.');
}
const name = 'dynamically_linked';
final logMessages = <String>[];
final logger = createCapturingLogger(logMessages);
final targetOS = OS.current;
final buildInputBuilder = BuildInputBuilder()
..setupShared(
packageName: name,
packageRoot: tempUri,
outputFile: tempUri.resolve('output.json'),
outputDirectoryShared: tempUri2,
)
..config.setupBuild(linkingEnabled: false)
..addExtension(
CodeAssetExtension(
targetOS: targetOS,
macOS: macOSConfig,
targetArchitecture: Architecture.current,
// Ignored by executables.
linkModePreference: LinkModePreference.dynamic,
cCompiler: cCompiler,
),
);
final buildInput = buildInputBuilder.build();
final buildOutput = BuildOutputBuilder();
final debugBuilder = CBuilder.library(
name: 'debug',
assetName: 'debug',
includes: [dynamicallyLinkedSrcUri.toFilePath()],
sources: [debugCUri.toFilePath()],
buildMode: BuildMode.release,
);
await debugBuilder.run(
input: buildInput,
output: buildOutput,
logger: logger,
);
final debugLibraryFile = File.fromUri(
buildInput.outputDirectory.resolve(OS.current.dylibFileName('debug')),
);
final nestedDebugLibraryFile = File.fromUri(
buildInput.outputDirectory
.resolve('debug/')
.resolve(OS.current.dylibFileName('debug')),
);
await nestedDebugLibraryFile.parent.create(recursive: true);
await debugLibraryFile.rename(nestedDebugLibraryFile.path);
final mathBuilder = CBuilder.library(
name: 'math',
assetName: 'math',
includes: [dynamicallyLinkedSrcUri.toFilePath()],
sources: [mathCUri.toFilePath()],
libraries: ['debug'],
libraryDirectories: ['debug'],
);
await mathBuilder.run(
input: buildInput,
output: buildOutput,
logger: logger,
);
await nestedDebugLibraryFile.rename(debugLibraryFile.path);
final executableBuilder = CBuilder.executable(
name: name,
includes: [dynamicallyLinkedSrcUri.toFilePath()],
sources: [dynamicallyLinkedCUri.toFilePath()],
libraries: ['math'],
);
await executableBuilder.run(
input: buildInput,
output: buildOutput,
logger: logger,
);
final executableUri = buildInput.outputDirectory.resolve(
OS.current.executableFileName(name),
);
expect(await File.fromUri(executableUri).exists(), true);
final result = await runProcess(executable: executableUri, logger: logger);
expect(result.exitCode, 0);
});
}
Future<void> testDefines({
BuildMode buildMode = BuildMode.debug,
bool buildModeDefine = false,
bool ndebugDefine = false,
bool? customDefineWithValue,
}) async {
final tempUri = await tempDirForTest();
final tempUri2 = 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 targetOS = OS.current;
final buildInputBuilder = BuildInputBuilder()
..setupShared(
packageName: name,
packageRoot: tempUri,
outputFile: tempUri.resolve('output.json'),
outputDirectoryShared: tempUri2,
)
..config.setupBuild(linkingEnabled: false)
..addExtension(
CodeAssetExtension(
targetOS: targetOS,
macOS: targetOS == OS.macOS
? MacOSCodeConfig(targetVersion: defaultMacOSVersion)
: null,
targetArchitecture: Architecture.current,
// Ignored by executables.
linkModePreference: LinkModePreference.dynamic,
cCompiler: cCompiler,
),
);
final buildInput = buildInputBuilder.build();
final buildOutput = BuildOutputBuilder();
final cbuilder = CBuilder.executable(
name: name,
sources: [definesCUri.toFilePath()],
defines: {
if (customDefineWithValue != null)
'FOO': customDefineWithValue ? 'BAR' : null,
},
buildModeDefine: buildModeDefine,
ndebugDefine: ndebugDefine,
buildMode: buildMode,
);
await cbuilder.run(input: buildInput, output: buildOutput, logger: logger);
final executableUri = buildInput.outputDirectory.resolve(
OS.current.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.'));
}
}