blob: 8e657cc9705892e887518060b5f7ec32958a1d8c [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:convert';
import 'package:cli_config/cli_config.dart';
import 'package:collection/collection.dart';
import 'package:crypto/crypto.dart';
import 'package:pub_semver/pub_semver.dart';
import '../utils/map.dart';
import '../utils/yaml.dart';
import 'ios_sdk.dart';
import 'link_mode_preference.dart';
import 'metadata.dart';
import 'target.dart';
class BuildConfig {
/// The folder in which all output and intermediate artifacts should be
/// placed.
Uri get outDir => _outDir;
late final Uri _outDir;
/// The root of the package the native assets are built for.
///
/// Often a package's native assets are built because a package is a
/// dependency of another. For this it is convenient to know the packageRoot.
Uri get packageRoot => _packageRoot;
late final Uri _packageRoot;
/// The target that is being compiled for.
Target get target => _target;
late final Target _target;
/// When compiling for iOS, whether to target device or simulator.
///
/// Required when [target.os] equals [OS.iOS].
IOSSdk? get targetIOSSdk => _targetIOSSdk;
late final IOSSdk? _targetIOSSdk;
/// When compiling for Android, the API version to target.
///
/// Required when [target.os] equals [OS.android].
int? get targetAndroidNdkApi => _targetAndroidNdkApi;
late final int? _targetAndroidNdkApi;
/// Preferred linkMode method for library.
LinkModePreference get linkModePreference => _linkModePreference;
late final LinkModePreference _linkModePreference;
/// Metadata from direct dependencies.
///
/// The key in the map is the package name of the dependency.
///
/// The key in the nested map is the key for the metadata from the dependency.
Map<String, Metadata>? get dependencyMetadata => _dependencyMetadata;
late final Map<String, Metadata>? _dependencyMetadata;
/// The configuration for invoking the C compiler.
CCompilerConfig get cCompiler => _cCompiler;
late final CCompilerConfig _cCompiler;
/// The underlying config.
///
/// Can be used for easier access to values on [dependencyMetadata].
Config get config => _config;
late final Config _config;
factory BuildConfig({
required Uri outDir,
required Uri packageRoot,
required Target target,
IOSSdk? targetIOSSdk,
int? targetAndroidNdkApi,
CCompilerConfig? cCompiler,
required LinkModePreference linkModePreference,
Map<String, Metadata>? dependencyMetadata,
}) {
final nonValidated = BuildConfig._()
.._outDir = outDir
.._packageRoot = packageRoot
.._target = target
.._targetIOSSdk = targetIOSSdk
.._targetAndroidNdkApi = targetAndroidNdkApi
.._cCompiler = cCompiler ?? CCompilerConfig()
.._linkModePreference = linkModePreference
.._dependencyMetadata = dependencyMetadata;
final parsedConfigFile = nonValidated.toYaml();
final config = Config(fileParsed: parsedConfigFile);
return BuildConfig.fromConfig(config);
}
/// Constructs a checksum for a [BuildConfig] based on the fields
/// of a buildconfig that influence the build.
///
/// This can be used for an [outDir].
///
/// In particular, it only takes the package name from [packageRoot],
/// so that the hash is equal across checkouts and ignores [outDir] itself.
static String checksum({
required Uri packageRoot,
required Target target,
IOSSdk? targetIOSSdk,
int? targetAndroidNdkApi,
CCompilerConfig? cCompiler,
required LinkModePreference linkModePreference,
Map<String, Metadata>? dependencyMetadata,
}) {
final packageName = packageRoot.pathSegments.lastWhere((e) => e.isNotEmpty);
final input = [
packageName,
target.toString(),
targetIOSSdk.toString(),
targetAndroidNdkApi.toString(),
linkModePreference.toString(),
cCompiler?.ar.toString(),
cCompiler?.cc.toString(),
cCompiler?.envScript.toString(),
cCompiler?.envScriptArgs.toString(),
cCompiler?.ld.toString(),
if (dependencyMetadata != null)
for (final entry in dependencyMetadata.entries) ...[
entry.key,
json.encode(entry.value.toYaml()),
]
].join('###');
final sha256String = sha256.convert(utf8.encode(input)).toString();
// 256 bit hashes lead to 64 hex character strings.
// To avoid overflowing file paths limits, only use 32.
// Using 16 hex characters would also be unlikely to have collisions.
const nameLength = 32;
return sha256String.substring(0, nameLength);
}
BuildConfig._();
/// The version of [BuildConfig].
///
/// This class is used in the protocol between the Dart and Flutter SDKs
/// and packages through `build.dart` invocations.
///
/// If we ever were to make breaking changes, it would be useful to give
/// proper error messages rather than just fail to parse the YAML
/// representation in the protocol.
static Version version = Version(1, 0, 0);
factory BuildConfig.fromConfig(Config config) {
final result = BuildConfig._().._cCompiler = CCompilerConfig._();
final configExceptions = <Object>[];
for (final f in result._readFieldsFromConfig()) {
try {
f(config);
} on FormatException catch (e, st) {
configExceptions.add(e);
configExceptions.add(st);
}
}
if (configExceptions.isNotEmpty) {
throw FormatException('Configuration is not in the right format. '
'FormatExceptions: $configExceptions');
}
return result;
}
/// Constructs a config by parsing CLI arguments and loading the config file.
///
/// The [args] must be commandline arguments.
///
/// If provided, [environment] must be a map containing environment variables.
/// If not provided, [environment] defaults to [Platform.environment].
///
/// If provided, [workingDirectory] is used to resolves paths inside
/// [environment].
/// If not provided, [workingDirectory] defaults to [Directory.current].
///
/// This async constructor is intended to be used directly in CLI files.
static Future<BuildConfig> fromArgs(
List<String> args, {
Map<String, String>? environment,
Uri? workingDirectory,
}) async {
final config = await Config.fromArgs(
args: args,
environment: environment,
workingDirectory: workingDirectory,
);
return BuildConfig.fromConfig(config);
}
static const outDirConfigKey = 'out_dir';
static const packageRootConfigKey = 'package_root';
static const dependencyMetadataConfigKey = 'dependency_metadata';
static const _versionKey = 'version';
static const targetAndroidNdkApiConfigKey = 'target_android_ndk_api';
List<void Function(Config)> _readFieldsFromConfig() {
var targetSet = false;
var ccSet = false;
return [
(config) {
final configVersion = Version.parse(config.string('version'));
if (configVersion.major > version.major) {
throw FormatException(
'The config version $configVersion is newer than this '
'package:native_assets_cli config version $version, '
'please update native_assets_cli.',
);
}
if (configVersion.major < version.major) {
throw FormatException(
'The config version $configVersion is newer than this '
'package:native_assets_cli config version $version, '
'please update the Dart or Flutter SDK.',
);
}
},
(config) => _config = config,
(config) => _outDir = config.path(outDirConfigKey, mustExist: true),
(config) =>
_packageRoot = config.path(packageRootConfigKey, mustExist: true),
(config) {
_target = Target.fromString(
config.string(
Target.configKey,
validValues: Target.values.map((e) => '$e'),
),
);
targetSet = true;
},
(config) => _targetIOSSdk = (targetSet && _target.os == OS.iOS)
? IOSSdk.fromString(
config.string(
IOSSdk.configKey,
validValues: IOSSdk.values.map((e) => '$e'),
),
)
: null,
(config) => _targetAndroidNdkApi = (targetSet && _target.os == OS.android)
? config.int(targetAndroidNdkApiConfigKey)
: null,
(config) => cCompiler._ar =
config.optionalPath(CCompilerConfig.arConfigKeyFull, mustExist: true),
(config) {
cCompiler._cc = config.optionalPath(CCompilerConfig.ccConfigKeyFull,
mustExist: true);
ccSet = true;
},
(config) => cCompiler._ld =
config.optionalPath(CCompilerConfig.ldConfigKeyFull, mustExist: true),
(config) => cCompiler._envScript = (ccSet &&
cCompiler.cc != null &&
cCompiler.cc!.toFilePath().endsWith('cl.exe'))
? config.path(CCompilerConfig.envScriptConfigKeyFull, mustExist: true)
: null,
(config) => cCompiler._envScriptArgs = config.optionalStringList(
CCompilerConfig.envScriptArgsConfigKeyFull,
splitEnvironmentPattern: ' ',
),
(config) => _linkModePreference = LinkModePreference.fromString(
config.string(
LinkModePreference.configKey,
validValues: LinkModePreference.values.map((e) => '$e'),
),
),
(config) =>
_dependencyMetadata = _readDependencyMetadataFromConfig(config)
];
}
Map<String, Metadata>? _readDependencyMetadataFromConfig(Config config) {
final fileValue =
config.valueOf<Map<Object?, Object?>?>(dependencyMetadataConfigKey);
if (fileValue == null) {
return null;
}
final result = <String, Metadata>{};
for (final entry in fileValue.entries) {
final packageName = entry.key;
final defines = entry.value;
if (defines is! Map) {
throw FormatException("Unexpected value '$defines' for key "
"'$dependencyMetadataConfigKey.$packageName' in config file. "
'Expected a Map.');
}
final packageResult = <String, Object>{};
for (final entry2 in defines.entries) {
final key = entry2.key;
assert(key is String);
final value = entry2.value;
assert(value != null);
packageResult[key as String] = value as Object;
}
result[packageName as String] = Metadata(packageResult.sortOnKey());
}
return result.sortOnKey();
}
Map<String, Object> toYaml() {
final cCompilerYaml = _cCompiler.toYaml();
return {
outDirConfigKey: _outDir.toFilePath(),
packageRootConfigKey: _packageRoot.toFilePath(),
Target.configKey: _target.toString(),
if (_targetIOSSdk != null) IOSSdk.configKey: _targetIOSSdk.toString(),
if (_targetAndroidNdkApi != null)
targetAndroidNdkApiConfigKey: _targetAndroidNdkApi!,
if (cCompilerYaml.isNotEmpty) CCompilerConfig.configKey: cCompilerYaml,
LinkModePreference.configKey: _linkModePreference.toString(),
if (_dependencyMetadata != null)
dependencyMetadataConfigKey: {
for (final entry in _dependencyMetadata!.entries)
entry.key: entry.value.toYaml(),
},
_versionKey: version.toString(),
}.sortOnKey();
}
String toYamlString() => yamlEncode(toYaml());
@override
bool operator ==(Object other) {
if (other is! BuildConfig) {
return false;
}
if (other._outDir != _outDir) return false;
if (other._packageRoot != _packageRoot) return false;
if (other._target != _target) return false;
if (other._targetIOSSdk != _targetIOSSdk) return false;
if (other._targetAndroidNdkApi != _targetAndroidNdkApi) return false;
if (other._cCompiler != _cCompiler) return false;
if (other._linkModePreference != _linkModePreference) return false;
if (!DeepCollectionEquality()
.equals(other._dependencyMetadata, _dependencyMetadata)) return false;
return true;
}
@override
int get hashCode => Object.hash(
_outDir,
_packageRoot,
_target,
_targetIOSSdk,
_targetAndroidNdkApi,
_cCompiler,
_linkModePreference,
DeepCollectionEquality().hash(_dependencyMetadata),
);
@override
String toString() => 'BuildConfig(${toYaml()})';
}
class CCompilerConfig {
/// Path to a C compiler.
Uri? get cc => _cc;
late final Uri? _cc;
/// Path to a native linker.
Uri? get ld => _ld;
late final Uri? _ld;
/// Path to a native archiver.
Uri? get ar => _ar;
late final Uri? _ar;
/// Path to script that sets environment variables for [cc], [ld], and [ar].
Uri? get envScript => _envScript;
late final Uri? _envScript;
/// Arguments for [envScript].
List<String>? get envScriptArgs => _envScriptArgs;
late final List<String>? _envScriptArgs;
factory CCompilerConfig({
Uri? ar,
Uri? cc,
Uri? ld,
Uri? envScript,
List<String>? envScriptArgs,
}) =>
CCompilerConfig._()
.._ar = ar
.._cc = cc
.._ld = ld
.._envScript = envScript
.._envScriptArgs = envScriptArgs;
CCompilerConfig._();
static const configKey = 'c_compiler';
static const arConfigKey = 'ar';
static const arConfigKeyFull = '$configKey.$arConfigKey';
static const ccConfigKey = 'cc';
static const ccConfigKeyFull = '$configKey.$ccConfigKey';
static const ldConfigKey = 'ld';
static const ldConfigKeyFull = '$configKey.$ldConfigKey';
static const envScriptConfigKey = 'env_script';
static const envScriptConfigKeyFull = '$configKey.$envScriptConfigKey';
static const envScriptArgsConfigKey = 'env_script_arguments';
static const envScriptArgsConfigKeyFull =
'$configKey.$envScriptArgsConfigKey';
Map<String, Object> toYaml() => {
if (_ar != null) arConfigKey: _ar!.toFilePath(),
if (_cc != null) ccConfigKey: _cc!.toFilePath(),
if (_ld != null) ldConfigKey: _ld!.toFilePath(),
if (_envScript != null) envScriptConfigKey: _envScript!.toFilePath(),
if (_envScriptArgs != null) envScriptArgsConfigKey: _envScriptArgs!,
}.sortOnKey();
@override
bool operator ==(Object other) {
if (other is! CCompilerConfig) {
return false;
}
if (other._ar != _ar) return false;
if (other._cc != _cc) return false;
if (other._ld != _ld) return false;
if (other.envScript != envScript) return false;
if (!ListEquality<String>().equals(other.envScriptArgs, envScriptArgs)) {
return false;
}
return true;
}
@override
int get hashCode => Object.hash(
_ar,
_cc,
_ld,
_envScript,
ListEquality<String>().hash(envScriptArgs),
);
}