blob: 1e078bec78b2af07e667162df8b32cc5f0913512 [file] [log] [blame]
// 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.
import 'dart:math';
import 'package:code_assets/code_assets.dart';
import 'package:hooks/hooks.dart';
import 'package:logging/logging.dart';
import '../native_toolchain/msvc.dart';
import '../native_toolchain/tool_likeness.dart';
import '../native_toolchain/xcode.dart';
import '../tool/tool_instance.dart';
import '../tool/tool_resolver.dart';
import '../utils/run_process.dart';
import 'compiler_resolver.dart';
import 'language.dart';
import 'linker_options.dart';
import 'optimization_level.dart';
class RunCBuilder {
/// The options are for linking only, so this will be non-null iff a linker
/// should be run.
final LinkerOptions? linkerOptions;
final HookInput input;
final CodeConfig codeConfig;
final Logger? logger;
final List<Uri> sources;
final List<Uri> includes;
final List<Uri> forcedIncludes;
final List<String> frameworks;
final List<String> libraries;
final List<Uri> libraryDirectories;
final Uri? executable;
final Uri? dynamicLibrary;
final Uri? staticLibrary;
final Uri outDir;
/// The install name of the [dynamicLibrary].
///
/// Can be inspected with `otool -D <path-to-dylib>`.
///
/// Can be modified with `install_name_tool`.
final Uri? installName;
final List<String> flags;
final Map<String, String?> defines;
final bool? pic;
final String? std;
final Language language;
final String? cppLinkStdLib;
final OptimizationLevel optimizationLevel;
RunCBuilder({
required this.input,
required this.codeConfig,
this.linkerOptions,
this.logger,
this.sources = const [],
this.includes = const [],
this.forcedIncludes = const [],
required this.frameworks,
this.libraries = const [],
this.libraryDirectories = 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,
required this.optimizationLevel,
}) : outDir = input.outputDirectory,
assert(
[executable, dynamicLibrary, staticLibrary].whereType<Uri>().length ==
1,
) {
if (codeConfig.targetOS == OS.windows && cppLinkStdLib != null) {
throw ArgumentError.value(
cppLinkStdLib,
'cppLinkStdLib',
'is not supported when targeting Windows',
);
}
}
late final _resolver = CompilerResolver(
codeConfig: codeConfig,
logger: logger,
);
Future<ToolInstance> compiler() async => await _resolver.resolveCompiler();
Future<Uri> archiver() async => (await _resolver.resolveArchiver()).uri;
Future<Uri> iosSdk(IOSSdk iosSdk, ToolResolvingContext context) async {
if (iosSdk == IOSSdk.iPhoneOS) {
return (await iPhoneOSSdk.defaultResolver!.resolve(
context,
)).where((i) => i.tool == iPhoneOSSdk).first.uri;
}
assert(iosSdk == IOSSdk.iPhoneSimulator);
return (await iPhoneSimulatorSdk.defaultResolver!.resolve(
context,
)).where((i) => i.tool == iPhoneSimulatorSdk).first.uri;
}
Future<Uri> macosSdk(ToolResolvingContext context) async =>
(await macosxSdk.defaultResolver!.resolve(
context,
)).where((i) => i.tool == macosxSdk).first.uri;
Uri androidSysroot(ToolInstance compiler) =>
compiler.uri.resolve('../sysroot/');
Future<void> run() async {
final toolInstance_ = await compiler();
final tool = toolInstance_.tool;
if (tool.isClangLike || tool.isLdLike) {
await runClangLike(tool: toolInstance_);
return;
} else if (tool == cl) {
await runCl(tool: toolInstance_);
} else {
throw UnimplementedError('This package does not know how to run $tool.');
}
}
Future<void> runClangLike({required ToolInstance tool}) async {
// Clang for Windows requires the MSVC Developer Environment.
final environment = await _resolver.resolveEnvironment(tool);
final isStaticLib = staticLibrary != null;
Uri? archiver_;
if (isStaticLib) {
archiver_ = await archiver();
}
final IOSSdk? targetIosSdk;
if (codeConfig.targetOS == OS.iOS) {
targetIosSdk = codeConfig.iOS.targetSdk;
} else {
targetIosSdk = null;
}
// The Android Gradle plugin does not honor API level 19 and 20 when
// invoking clang. Mimic that behavior here.
// See https://github.com/dart-lang/native/issues/171.
final int? targetAndroidNdkApi;
if (codeConfig.targetOS == OS.android) {
final minimumApi = codeConfig.targetArchitecture == Architecture.riscv64
? 35
: 21;
targetAndroidNdkApi = max(codeConfig.android.targetNdkApi, minimumApi);
} else {
targetAndroidNdkApi = null;
}
final targetIOSVersion = codeConfig.targetOS == OS.iOS
? codeConfig.iOS.targetVersion
: null;
final targetMacOSVersion = codeConfig.targetOS == OS.macOS
? codeConfig.macOS.targetVersion
: null;
final architecture = codeConfig.targetArchitecture;
final sourceFiles = sources.map((e) => e.toFilePath()).toList();
final objectFiles = <Uri>[];
if (staticLibrary != null) {
for (var i = 0; i < sourceFiles.length; i++) {
final objectFile = outDir.resolve('out$i.o');
await _compile(
tool,
architecture,
targetAndroidNdkApi,
targetIosSdk,
targetIOSVersion,
targetMacOSVersion,
[sourceFiles[i]],
objectFile,
environment,
);
objectFiles.add(objectFile);
}
await runProcess(
executable: archiver_!,
arguments: [
'rc',
outDir.resolveUri(staticLibrary!).toFilePath(),
...objectFiles.map((objectFile) => objectFile.toFilePath()),
],
logger: logger,
captureOutput: false,
throwOnUnexpectedExitCode: true,
environment: environment,
);
} else {
await _compile(
tool,
architecture,
targetAndroidNdkApi,
targetIosSdk,
targetIOSVersion,
targetMacOSVersion,
sourceFiles,
dynamicLibrary != null ? outDir.resolveUri(dynamicLibrary!) : null,
environment,
);
}
}
/// [toolInstance] is either a compiler or a linker.
Future<void> _compile(
ToolInstance toolInstance,
Architecture? architecture,
int? targetAndroidNdkApi,
IOSSdk? targetIosSdk,
int? targetIOSVersion,
int? targetMacOSVersion,
Iterable<String> sourceFiles,
Uri? outFile,
Map<String, String> environment,
) async {
final context = ToolResolvingContext(logger: logger);
await runProcess(
executable: toolInstance.uri,
environment: environment,
arguments: [
if (codeConfig.targetOS == OS.android) ...[
'--target='
'${androidNdkClangTargetFlags[architecture]!}'
'${targetAndroidNdkApi!}',
'--sysroot=${androidSysroot(toolInstance).toFilePath()}',
],
if (codeConfig.targetOS == OS.windows)
'--target=${clangWindowsTargetFlags[architecture]!}',
if (codeConfig.targetOS == OS.macOS)
'--target=${appleClangMacosTargetFlags[architecture]!}',
if (codeConfig.targetOS == OS.iOS)
'--target=${appleClangIosTargetFlags[architecture]![targetIosSdk]!}',
if (targetIOSVersion != null) '-mios-version-min=$targetIOSVersion',
if (targetMacOSVersion != null)
'-mmacos-version-min=$targetMacOSVersion',
if (codeConfig.targetOS == OS.iOS) ...[
'-isysroot',
(await iosSdk(targetIosSdk!, context)).toFilePath(),
],
if (codeConfig.targetOS == OS.macOS) ...[
'-isysroot',
(await macosSdk(context)).toFilePath(),
],
if (installName != null) ...[
'-install_name',
installName!.toFilePath(),
],
if (pic != null)
if (toolInstance.tool.isClangLike &&
codeConfig.targetOS != OS.windows) ...[
if (pic!) ...[
if (dynamicLibrary != null) '-fPIC',
// Using PIC for static libraries allows them to be linked into
// any executable, but it is not necessarily the best option in
// terms of overhead. We would have to know whether the target
// into which the static library is linked is PIC, PIE or neither.
// Then we could use the same option for the static library.
if (staticLibrary != null) '-fPIC',
if (executable != null) ...[
// Generate position-independent code for executables.
'-fPIE',
// Tell the linker to generate a position-independent
// executable.
'-pie',
],
] else ...[
// Disable generation of any kind of position-independent code.
'-fno-PIC',
'-fno-PIE',
// Tell the linker to generate a position-dependent executable.
if (executable != null) '-no-pie',
],
] else if (toolInstance.tool.isLdLike) ...[
if (pic!) ...[
if (executable != null) '--pie',
] else ...[
if (executable != null) '--no-pie',
],
],
if (std != null) '-std=$std',
if (language == Language.cpp) ...[
'-x',
'c++',
'-l',
cppLinkStdLib ?? defaultCppLinkStdLib[codeConfig.targetOS]!,
],
if (optimizationLevel != OptimizationLevel.unspecified)
optimizationLevel.clangFlag(),
// Support Android 15 page size by default, can be overridden by
// passing [flags].
if (codeConfig.targetOS == OS.android) '-Wl,-z,max-page-size=16384',
...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()}',
for (final forcedInclude in forcedIncludes)
'-include${forcedInclude.toFilePath()}',
if (linkerOptions != null)
...linkerOptions!.sourceFilesToFlags(
toolInstance.tool,
sourceFiles,
codeConfig.targetOS,
codeConfig.targetArchitecture,
)
else
...sourceFiles,
if (language == Language.objectiveC) ...[
for (final framework in frameworks) ...['-framework', framework],
],
if (executable != null) ...[
'-o',
outDir.resolveUri(executable!).toFilePath(),
] else if (dynamicLibrary != null) ...[
'--shared',
'-o',
outFile!.toFilePath(),
] else if (staticLibrary != null) ...[
'-c',
'-o',
outFile!.toFilePath(),
],
if (executable != null || dynamicLibrary != null) ...[
if (codeConfig.targetOS case OS.android || OS.linux)
// During bundling code assets are all placed in the same directory.
// Setting this rpath allows the binary to find other code assets
// it is linked against.
'-Wl,-rpath=\$ORIGIN',
for (final directory in libraryDirectories)
'-L${directory.toFilePath()}',
for (final library in libraries) '-l$library',
],
],
logger: logger,
captureOutput: false,
throwOnUnexpectedExitCode: true,
);
}
Future<void> runCl({required ToolInstance tool}) async {
final environment = await _resolver.resolveEnvironment(tool);
final isStaticLib = staticLibrary != null;
Uri? archiver_;
if (isStaticLib) {
archiver_ = await archiver();
}
final sourceFiles = sources.map((e) => e.toFilePath());
final result = await runProcess(
executable: tool.uri,
arguments: [
if (optimizationLevel != OptimizationLevel.unspecified)
optimizationLevel.msvcFlag(),
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()}',
for (final forcedInclude in forcedIncludes)
'/FI${forcedInclude.toFilePath()}',
if (executable != null) ...[
'/Fe:${outDir.resolveUri(executable!).toFilePath()}',
] else if (dynamicLibrary != null) ...[
'/LD',
'/Fe:${outDir.resolveUri(dynamicLibrary!).toFilePath()}',
] else if (staticLibrary != null) ...[
'/c',
],
if (linkerOptions != null)
...linkerOptions!.sourceFilesToFlags(
tool.tool,
sourceFiles,
codeConfig.targetOS,
codeConfig.targetArchitecture,
)
else ...[
...sourceFiles,
'/link',
],
'/MACHINE:${clTargetFlags[codeConfig.targetArchitecture]}',
if (executable != null || dynamicLibrary != null) ...[
for (final directory in libraryDirectories)
'/LIBPATH:${directory.toFilePath()}',
for (final library in libraries) '$library.lib',
],
],
workingDirectory: outDir,
environment: environment,
logger: logger,
captureOutput: false,
stdoutLogLevel: Level.INFO,
throwOnUnexpectedExitCode: true,
);
if (staticLibrary != null) {
await runProcess(
executable: archiver_!,
arguments: ['/out:${staticLibrary!.toFilePath()}', '*.obj'],
workingDirectory: outDir,
environment: environment,
logger: logger,
captureOutput: false,
stdoutLogLevel: Level.INFO,
throwOnUnexpectedExitCode: true,
);
}
assert(result.exitCode == 0);
}
static const androidNdkClangTargetFlags = {
Architecture.arm: 'armv7a-linux-androideabi',
Architecture.arm64: 'aarch64-linux-android',
Architecture.ia32: 'i686-linux-android',
Architecture.x64: 'x86_64-linux-android',
Architecture.riscv64: 'riscv64-linux-android',
};
static const appleClangMacosTargetFlags = {
Architecture.arm64: 'arm64-apple-darwin',
Architecture.x64: 'x86_64-apple-darwin',
};
static const appleClangIosTargetFlags = {
Architecture.arm64: {
IOSSdk.iPhoneOS: 'arm64-apple-ios',
IOSSdk.iPhoneSimulator: 'arm64-apple-ios-simulator',
},
Architecture.x64: {IOSSdk.iPhoneSimulator: 'x86_64-apple-ios-simulator'},
};
static const clangWindowsTargetFlags = {
Architecture.arm64: 'arm64-pc-windows-msvc',
Architecture.ia32: 'i386-pc-windows-msvc',
Architecture.x64: 'x86_64-pc-windows-msvc',
};
static const clTargetFlags = {
Architecture.arm64: 'ARM64',
Architecture.ia32: 'X86',
Architecture.x64: 'X64',
};
static const defaultCppLinkStdLib = {
OS.android: 'c++_shared',
OS.fuchsia: 'c++',
OS.iOS: 'c++',
OS.linux: 'stdc++',
OS.macOS: 'c++',
};
}