blob: b310704c060691cd9b3ad76ca5ce87227f4907b0 [file] [edit]
// Copyright (c) 2026, 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:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:logging/logging.dart';
import 'package:native_toolchain_c/native_toolchain_c.dart';
import 'package:native_toolchain_c/src/cbuilder/compiler_resolver.dart';
import 'package:path/path.dart' as p;
// All ObjC source files are compiled with ARC enabled except these.
const arcDisabledFiles = <String>{'ref_count_test.m'};
final logger = Logger('')
..level = Level.INFO
..onRecord.listen((record) {
print('${record.level.name}: ${record.time}: ${record.message}');
});
void main(List<String> args) async {
await build(args, (input, output) async {
if (input.userDefines['include_test_utils'] != true) {
return;
}
if (!input.config.buildCodeAssets) {
return;
}
final packageName = input.packageName;
final codeConfig = input.config.code;
final cFlags = codeConfig.targetOS == OS.macOS
? <String>[
'-isysroot',
sdkPath(codeConfig),
if (codeConfig.targetArchitecture == Architecture.arm64) ...[
'-arch',
'arm64e',
] else ...[
'-target',
toTargetTriple(codeConfig),
],
minOSVersion(codeConfig),
]
: <String>[];
// Build native_test.c. This is an ordinary build, so we can use CBuilder.
await CBuilder.library(
name: 'native_test',
assetName: 'native_test',
sources: [
'test/native_test/native_test.c',
if (codeConfig.targetOS == OS.windows)
'test/native_test/native_test.def',
],
flags: cFlags,
).run(input: input, output: output, logger: logger);
// Build all C++ test files. Works on all platforms.
final cppTestDir = input.packageRoot.resolve('test/native_cpp_test/');
final cppFiles = _findFiles(cppTestDir, '.cpp')
.map(
(uri) => p.relative(
uri.toFilePath(),
from: input.packageRoot.toFilePath(),
),
)
.toList();
await CBuilder.library(
name: 'cpp_test',
assetName: 'cpp_test',
sources: cppFiles,
language: Language.cpp,
std: 'c++17',
flags: cFlags,
).run(input: input, output: output, logger: logger);
if (codeConfig.targetOS == OS.macOS) {
final builder = await CustomBuilder.create(
input,
input.packageRoot.toFilePath(),
);
// Build swift_class_test.swift. There's no swift compilation package, so
// we have to use a CustomBuilder.
final objcTestDir = input.packageRoot.resolve('test/native_objc_test/');
const swiftModule = 'swift_class_test';
final swiftFile = objcTestDir.resolve('swift_class_test.swift');
final swiftHeader = objcTestDir.resolve('swift_class_test-Swift.h');
final swiftLib = input.outputDirectory.resolve('$swiftModule.dylib');
final swiftBindingM = objcTestDir.resolve('swift_class_test_bindings.m');
final extraObjects = <String>[];
if (File.fromUri(swiftBindingM).existsSync()) {
extraObjects.add(
await builder.buildObject(swiftBindingM, [
'-x',
'objective-c',
'-fobjc-arc',
'-Wno-nullability-completeness',
]),
);
output.dependencies.add(swiftBindingM);
}
await builder.buildSwift(
swiftFile,
swiftModule,
swiftHeader,
swiftLib,
extraObjects: extraObjects,
);
output.assets.code.add(
CodeAsset(
package: packageName,
name: swiftModule,
file: swiftLib,
linkMode: DynamicLoadingBundled(),
),
);
// Build all the ObjC files. Some of the files have different compile
// flags, so we need to use the CustomBuilder again.
final mFiles = _findFiles(objcTestDir, '.m')
.where((uri) => !uri.pathSegments.last.contains('swift_class_test'))
.toList();
final hFiles = _findFiles(objcTestDir, '.h');
final objFiles = <String>[];
for (final mFile in mFiles) {
final useArc = !arcDisabledFiles.contains(mFile.pathSegments.last);
objFiles.add(
await builder.buildObject(mFile, [
...cFlags,
'-x',
'objective-c',
if (useArc) '-fobjc-arc',
'-Wno-nullability-completeness',
'-DDISABLE_METHOD',
]),
);
}
// Add dart_api_dl.c from objective_c package.
final dartApiDl = input.packageRoot.resolve(
'../objective_c/src/include/dart_api_dl.c',
);
objFiles.add(await builder.buildObject(dartApiDl, cFlags));
const objcAsset = 'objc_test';
final objcLib = input.outputDirectory.resolve('$objcAsset.dylib');
await builder.linkLib(objFiles, objcLib, [
...cFlags,
'-framework',
'Foundation',
]);
output.dependencies.addAll([...mFiles, ...hFiles, swiftFile, dartApiDl]);
output.assets.code.add(
CodeAsset(
package: packageName,
name: objcAsset,
file: objcLib,
linkMode: DynamicLoadingBundled(),
),
);
}
});
}
String sdkPath(CodeConfig codeConfig) {
final String target;
if (codeConfig.targetOS == OS.iOS) {
if (codeConfig.iOS.targetSdk == IOSSdk.iPhoneOS) {
target = 'iphoneos';
} else {
target = 'iphonesimulator';
}
} else {
assert(codeConfig.targetOS == OS.macOS);
target = 'macosx';
}
return firstLineOfStdout('xcrun', ['--show-sdk-path', '--sdk', target]);
}
String firstLineOfStdout(String cmd, List<String> args) {
final result = Process.runSync(cmd, args);
if (result.exitCode != 0) {
throw Exception(
'Command failed: $cmd ${args.join(" ")}\n'
'${result.stdout}\n${result.stderr}',
);
}
return (result.stdout as String)
.split('\n')
.where((line) => line.isNotEmpty)
.first;
}
String minOSVersion(CodeConfig codeConfig) {
if (codeConfig.targetOS == OS.iOS) {
final targetVersion = codeConfig.iOS.targetVersion;
return '-mios-version-min=$targetVersion';
}
assert(codeConfig.targetOS == OS.macOS);
final targetVersion = codeConfig.macOS.targetVersion;
return '-mmacos-version-min=$targetVersion';
}
String toTargetTriple(CodeConfig codeConfig) {
final architecture = codeConfig.targetArchitecture;
if (codeConfig.targetOS == OS.iOS) {
return appleClangIosTargetFlags[architecture]![codeConfig.iOS.targetSdk]!;
}
assert(codeConfig.targetOS == OS.macOS);
return appleClangMacosTargetFlags[architecture]!;
}
final appleClangMacosTargetFlags = {
Architecture.arm64: 'arm64-apple-darwin',
Architecture.x64: 'x86_64-apple-darwin',
};
final appleClangIosTargetFlags = {
Architecture.arm64: {
IOSSdk.iPhoneOS: 'arm64-apple-ios',
IOSSdk.iPhoneSimulator: 'arm64-apple-ios-simulator',
},
Architecture.x64: {IOSSdk.iPhoneSimulator: 'x86_64-apple-ios-simulator'},
};
List<Uri> _findFiles(Uri dir, String suffix) => Directory.fromUri(dir)
.listSync()
.whereType<File>()
.map((f) => f.uri)
.where((uri) => uri.pathSegments.last.endsWith(suffix))
.toList();
class CustomBuilder {
final String _comp;
final String _rootDir;
final Uri _tempOutDir;
final CodeConfig _codeConfig;
CustomBuilder._(
this._comp,
this._rootDir,
this._tempOutDir,
this._codeConfig,
);
static Future<CustomBuilder> create(BuildInput input, String rootDir) async {
final resolver = CompilerResolver(
codeConfig: input.config.code,
logger: logger,
);
return CustomBuilder._(
(await resolver.resolveCompiler()).uri.toFilePath(),
rootDir,
input.outputDirectory.resolve('obj/'),
input.config.code,
);
}
Future<String> buildObject(Uri input, List<String> flags) async {
assert(input.toFilePath().startsWith(_rootDir));
final relativeInput = p.relative(input.toFilePath(), from: _rootDir);
final output = '${_tempOutDir.resolve(relativeInput).toFilePath()}.o';
File(output).parent.createSync(recursive: true);
await _runClang([
...flags,
'-c',
input.toFilePath(),
'-fpic',
'-gline-tables-only',
], output);
return output;
}
Future<void> linkLib(List<String> objects, Uri output, List<String> flags) =>
_runClang(['-shared', ...flags, ...objects], output.toFilePath());
Future<void> _runClang(List<String> flags, String output) =>
_run(_comp, [...flags, '-o', output]);
Future<void> buildSwift(
Uri input,
String moduleName,
Uri outputHeader,
Uri outputLib, {
List<String> extraObjects = const [],
}) async {
final baseArgs = [
'-c',
input.toFilePath(),
...extraObjects,
'-module-name',
moduleName,
'-emit-library',
];
final String target;
if (_codeConfig.targetOS == OS.iOS) {
final version = _codeConfig.iOS.targetVersion;
final arch = _codeConfig.targetArchitecture == Architecture.x64
? 'x86_64'
: 'arm64';
final sdk = _codeConfig.iOS.targetSdk == IOSSdk.iPhoneOS
? 'ios'
: 'ios-simulator';
target = '$arch-apple-$sdk$version';
} else {
final version = _codeConfig.macOS.targetVersion;
final arch = _codeConfig.targetArchitecture == Architecture.x64
? 'x86_64'
: 'arm64';
target = '$arch-apple-macosx$version';
}
if (_codeConfig.targetOS == OS.macOS &&
_codeConfig.targetArchitecture == Architecture.arm64) {
final version = _codeConfig.macOS.targetVersion;
await _run('swiftc', [
...baseArgs,
'-emit-objc-header-path',
outputHeader.toFilePath(),
'-target',
'arm64e-apple-macosx$version',
'-o',
outputLib.toFilePath(),
]);
} else {
await _run('swiftc', [
...baseArgs,
'-emit-objc-header-path',
outputHeader.toFilePath(),
'-target',
target,
'-o',
outputLib.toFilePath(),
]);
}
}
Future<void> _run(String cmd, List<String> args) async {
final proc = await Process.run(cmd, args);
if (proc.exitCode != 0) {
throw Exception(
'Command failed: $cmd ${args.join(" ")}\n'
'${proc.stdout}\n${proc.stderr}',
);
}
}
}