blob: e11512422b786325e189f1ab1223304ee2bc99dd [file]
// 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;
// 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 (input.config.code.targetOS == OS.windows)
'test/native_test/native_test.def',
],
).run(input: input, output: output, logger: logger);
if (input.config.code.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');
await builder.buildSwift(swiftFile, swiftModule, swiftHeader, swiftLib);
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');
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, [
'-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, []));
const objcAsset = 'objc_test';
final objcLib = input.outputDirectory.resolve('$objcAsset.dylib');
await builder.linkLib(objFiles, objcLib, ['-framework', 'Foundation']);
output.dependencies.addAll([...mFiles, ...hFiles, swiftFile, dartApiDl]);
output.assets.code.add(
CodeAsset(
package: packageName,
name: objcAsset,
file: objcLib,
linkMode: DynamicLoadingBundled(),
),
);
}
});
}
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;
CustomBuilder._(this._comp, this._rootDir, this._tempOutDir);
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/'),
);
}
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,
) async {
final args = [
'-c',
input.toFilePath(),
'-module-name',
moduleName,
'-emit-library',
'-emit-objc-header-path',
outputHeader.toFilePath(),
'-o',
outputLib.toFilePath(),
];
await _run('swiftc', args);
}
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}',
);
}
}
}