// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:io'; import 'package:code_assets/code_assets.dart'; import 'package:hooks/hooks.dart'; import 'package:logging/logging.dart'; import 'package:native_toolchain_c/src/cbuilder/compiler_resolver.dart'; const objCFlags = ['-x', 'objective-c', '-fobjc-arc']; const assetName = 'objective_c.dylib'; // TODO(https://github.com/dart-lang/native/issues/2272): Remove this from the // main build. const testFiles = ['test/util.c']; final logger = Logger('') ..level = Level.INFO ..onRecord.listen((record) { print('${record.level.name}: ${record.time}: ${record.message}'); }); void main(List args) async { await build(args, (input, output) async { if (!input.config.buildCodeAssets) { // Don't build any other asset types. return; } const supportedOSs = {OS.iOS, OS.macOS}; final codeConfig = input.config.code; final os = codeConfig.targetOS; if (!supportedOSs.contains(os)) { // Nothing to do. return; } if (codeConfig.linkModePreference == LinkModePreference.static) { throw UnsupportedError('LinkModePreference.static is not supported.'); } final packageName = input.packageName; final assetPath = input.outputDirectory.resolve(assetName); final srcDir = Directory.fromUri(input.packageRoot.resolve('src/')); final target = toTargetTriple(codeConfig); final cFiles = []; final mFiles = []; final hFiles = []; for (final file in srcDir.listSync(recursive: true)) { if (file is File) { final path = file.path; if (path.endsWith('.c')) cFiles.add(path); if (path.endsWith('.m')) mFiles.add(path); if (path.endsWith('.h')) hFiles.add(path); } } // Only include the test utils on mac OS. They use memory functions that // aren't supported on iOS, like mach_vm_region. We don't need them on iOS // anyway since we only run memory tests on mac. if (os == OS.macOS) { cFiles.addAll( testFiles.map((f) => input.packageRoot.resolve(f).toFilePath()), ); } final sysroot = sdkPath(codeConfig); final minVersion = minOSVersion(codeConfig); final cFlags = [ '-isysroot', sysroot, '-target', target, minVersion, ]; final mFlags = [...cFlags, ...objCFlags]; final linkFlags = cFlags; final builder = await Builder.create(input, input.packageRoot.toFilePath()); final objectFiles = await Future.wait(>[ for (final src in cFiles) builder.buildObject(src, cFlags), for (final src in mFiles) builder.buildObject(src, mFlags), ]); await builder.linkLib(objectFiles, assetPath.toFilePath(), linkFlags); output.dependencies.addAll(cFiles.map(Uri.file)); output.dependencies.addAll(mFiles.map(Uri.file)); output.dependencies.addAll(hFiles.map(Uri.file)); output.assets.code.add( CodeAsset( package: packageName, name: assetName, file: assetPath, linkMode: DynamicLoadingBundled(), ), ); }); } class Builder { final String _comp; final String _rootDir; final Uri _tempOutDir; Builder._(this._comp, this._rootDir, this._tempOutDir); static Future create(BuildInput input, String rootDir) async { final resolver = CompilerResolver( codeConfig: input.config.code, logger: logger, ); return Builder._( (await resolver.resolveCompiler()).uri.toFilePath(), rootDir, input.outputDirectory.resolve('obj/'), ); } Future buildObject(String input, List flags) async { assert(input.startsWith(_rootDir)); final relativeInput = input.substring(_rootDir.length); final output = '${_tempOutDir.resolve(relativeInput).toFilePath()}.o'; File(output).parent.createSync(recursive: true); await _compile([...flags, '-c', input, '-fpic', '-I', 'src'], output); return output; } Future linkLib( List objects, String output, List flags, ) => _compile([ '-shared', '-Wl,-encryptable', '-undefined', 'dynamic_lookup', ...flags, ...objects, ], output); Future _compile(List flags, String output) async { final args = [...flags, '-o', output]; logger.info('Running: $_comp ${args.join(" ")}'); final proc = await Process.run(_comp, args); logger.info(proc.stdout); logger.info(proc.stderr); if (proc.exitCode != 0) { exitCode = proc.exitCode; throw Exception('Command failed: $_comp ${args.join(" ")}'); } logger.info('Generated $output'); } } 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 args) { final result = Process.runSync(cmd, args); assert(result.exitCode == 0); 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]!; } const appleClangMacosTargetFlags = { Architecture.arm64: 'arm64-apple-darwin', Architecture.x64: 'x86_64-apple-darwin', }; const appleClangIosTargetFlags = { Architecture.arm64: { IOSSdk.iPhoneOS: 'arm64-apple-ios', IOSSdk.iPhoneSimulator: 'arm64-apple-ios-simulator', }, Architecture.x64: {IOSSdk.iPhoneSimulator: 'x86_64-apple-ios-simulator'}, };