blob: 93e0d9d4568176e5cec22689648f10d696b10706 [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 'dart:io';
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 'build_mode.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 name of the package the native assets are built for.
String get packageName => _packageName;
late final String _packageName;
/// 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 being compiled for.
///
/// Not available in [dryRun].
late final Target target =
Target.fromArchitectureAndOs(targetArchitecture, targetOs);
/// The architecture being compiled for.
///
/// Not available in [dryRun].
Architecture get targetArchitecture {
_ensureNotDryRun();
return _targetArchitecture;
}
late final Architecture _targetArchitecture;
/// The operating system being compiled for.
OS get targetOs => _targetOs;
late final OS _targetOs;
/// When compiling for iOS, whether to target device or simulator.
///
/// Required when [targetOs] equals [OS.iOS].
///
/// Not available in [dryRun].s
IOSSdk? get targetIOSSdk {
_ensureNotDryRun();
return _targetIOSSdk;
}
late final IOSSdk? _targetIOSSdk;
/// When compiling for Android, the minimum Android SDK API version to that
/// the compiled code will be compatible with.
///
/// Required when [targetOs] equals [OS.android].
///
/// Not available in [dryRun].
///
/// For more information about the Android API version, refer to
/// [`minSdkVersion`](https://developer.android.com/ndk/guides/sdk-versions#minsdkversion)
/// in the Android documentation.
int? get targetAndroidNdkApi {
_ensureNotDryRun();
return _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.
///
/// Not available in [dryRun].
Map<String, Metadata>? get dependencyMetadata {
_ensureNotDryRun();
return _dependencyMetadata;
}
late final Map<String, Metadata>? _dependencyMetadata;
/// The configuration for invoking the C compiler.
///
/// Not available in [dryRun].
CCompilerConfig get cCompiler {
_ensureNotDryRun();
return _cCompiler;
}
late final CCompilerConfig _cCompiler;
/// Don't run the build, only report the native assets produced.
bool get dryRun => _dryRun ?? false;
late final bool? _dryRun;
/// The build mode that the code should be compiled in.
///
/// Not available in [dryRun].
BuildMode get buildMode {
_ensureNotDryRun();
return _buildMode;
}
late final BuildMode _buildMode;
/// 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 String packageName,
required Uri packageRoot,
required BuildMode buildMode,
required Architecture targetArchitecture,
required OS targetOs,
IOSSdk? targetIOSSdk,
int? targetAndroidNdkApi,
CCompilerConfig? cCompiler,
required LinkModePreference linkModePreference,
Map<String, Metadata>? dependencyMetadata,
}) {
final nonValidated = BuildConfig._()
.._outDir = outDir
.._packageName = packageName
.._packageRoot = packageRoot
.._buildMode = buildMode
.._targetArchitecture = targetArchitecture
.._targetOs = targetOs
.._targetIOSSdk = targetIOSSdk
.._targetAndroidNdkApi = targetAndroidNdkApi
.._cCompiler = cCompiler ?? CCompilerConfig()
.._linkModePreference = linkModePreference
.._dependencyMetadata = dependencyMetadata
.._dryRun = false;
final parsedConfigFile = nonValidated.toYaml();
final config = Config(fileParsed: parsedConfigFile);
return BuildConfig.fromConfig(config);
}
factory BuildConfig.dryRun({
required Uri outDir,
required String packageName,
required Uri packageRoot,
required OS targetOs,
required LinkModePreference linkModePreference,
}) {
final nonValidated = BuildConfig._()
.._outDir = outDir
.._packageName = packageName
.._packageRoot = packageRoot
.._targetOs = targetOs
.._linkModePreference = linkModePreference
.._cCompiler = CCompilerConfig()
.._dryRun = true;
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], but should not be used for dry-runs.
///
/// 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 String packageName,
required Uri packageRoot,
required Architecture targetArchitecture,
required OS targetOs,
required BuildMode buildMode,
IOSSdk? targetIOSSdk,
int? targetAndroidNdkApi,
CCompilerConfig? cCompiler,
required LinkModePreference linkModePreference,
Map<String, Metadata>? dependencyMetadata,
}) {
final input = [
packageName,
targetArchitecture.toString(),
targetOs.toString(),
targetIOSSdk.toString(),
targetAndroidNdkApi.toString(),
buildMode.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 packageNameConfigKey = 'package_name';
static const packageRootConfigKey = 'package_root';
static const dependencyMetadataConfigKey = 'dependency_metadata';
static const _versionKey = 'version';
static const targetAndroidNdkApiConfigKey = 'target_android_ndk_api';
static const dryRunConfigKey = 'dry_run';
List<void Function(Config)> _readFieldsFromConfig() {
var osSet = 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) => _dryRun = config.optionalBool(dryRunConfigKey),
(config) => _outDir = config.path(outDirConfigKey, mustExist: true),
(config) => _packageName = config.string(packageNameConfigKey),
(config) =>
_packageRoot = config.path(packageRootConfigKey, mustExist: true),
(config) {
if (dryRun) {
_throwIfNotNullInDryRun<String>(BuildMode.configKey);
} else {
_buildMode = BuildMode.fromString(
config.string(
BuildMode.configKey,
validValues: BuildMode.values.map((e) => '$e'),
),
);
}
},
(config) {
_targetOs = OS.fromString(
config.string(
OS.configKey,
validValues: OS.values.map((e) => '$e'),
),
);
osSet = true;
},
(config) {
if (dryRun) {
_throwIfNotNullInDryRun<String>(Architecture.configKey);
} else {
final validArchitectures = [
if (!osSet)
...Architecture.values
else
for (final target in Target.values)
if (target.os == _targetOs) target.architecture
];
_targetArchitecture = Architecture.fromString(
config.string(
Architecture.configKey,
validValues: validArchitectures.map((e) => '$e'),
),
);
}
},
(config) {
if (dryRun) {
_throwIfNotNullInDryRun<String>(IOSSdk.configKey);
} else {
_targetIOSSdk = (osSet && _targetOs == OS.iOS)
? IOSSdk.fromString(
config.string(
IOSSdk.configKey,
validValues: IOSSdk.values.map((e) => '$e'),
),
)
: null;
}
},
(config) {
if (dryRun) {
_throwIfNotNullInDryRun<int>(targetAndroidNdkApiConfigKey);
} else {
_targetAndroidNdkApi = (osSet && _targetOs == OS.android)
? config.int(targetAndroidNdkApiConfigKey)
: null;
}
},
(config) {
if (dryRun) {
_throwIfNotNullInDryRun<int>(CCompilerConfig.arConfigKeyFull);
} else {
cCompiler._ar = config.optionalPath(
CCompilerConfig.arConfigKeyFull,
mustExist: true,
);
}
},
(config) {
if (dryRun) {
_throwIfNotNullInDryRun<int>(CCompilerConfig.ccConfigKeyFull);
} else {
cCompiler._cc = config.optionalPath(
CCompilerConfig.ccConfigKeyFull,
mustExist: true,
);
ccSet = true;
}
},
(config) {
if (dryRun) {
_throwIfNotNullInDryRun<int>(CCompilerConfig.ccConfigKeyFull);
} else {
cCompiler._ld = config.optionalPath(
CCompilerConfig.ldConfigKeyFull,
mustExist: true,
);
}
},
(config) {
if (dryRun) {
_throwIfNotNullInDryRun<int>(CCompilerConfig.ccConfigKeyFull);
} else {
cCompiler._envScript = (ccSet &&
cCompiler.cc != null &&
cCompiler.cc!.toFilePath().endsWith('cl.exe'))
? config.path(CCompilerConfig.envScriptConfigKeyFull,
mustExist: true)
: null;
}
},
(config) {
if (dryRun) {
_throwIfNotNullInDryRun<int>(CCompilerConfig.ccConfigKeyFull);
} else {
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 = as<String>(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 = as<String>(entry2.key);
final value = as<Object>(entry2.value);
packageResult[key] = value;
}
result[packageName] = Metadata(packageResult.sortOnKey());
}
return result.sortOnKey();
}
Map<String, Object> toYaml() {
late Map<String, Object> cCompilerYaml;
if (!dryRun) {
cCompilerYaml = _cCompiler.toYaml();
}
return {
outDirConfigKey: _outDir.toFilePath(),
packageNameConfigKey: _packageName,
packageRootConfigKey: _packageRoot.toFilePath(),
OS.configKey: _targetOs.toString(),
LinkModePreference.configKey: _linkModePreference.toString(),
_versionKey: version.toString(),
if (dryRun) dryRunConfigKey: dryRun,
if (!dryRun) ...{
BuildMode.configKey: _buildMode.toString(),
Architecture.configKey: _targetArchitecture.toString(),
if (_targetIOSSdk != null) IOSSdk.configKey: _targetIOSSdk.toString(),
if (_targetAndroidNdkApi != null)
targetAndroidNdkApiConfigKey: _targetAndroidNdkApi!,
if (cCompilerYaml.isNotEmpty) CCompilerConfig.configKey: cCompilerYaml,
if (_dependencyMetadata != null)
dependencyMetadataConfigKey: {
for (final entry in _dependencyMetadata!.entries)
entry.key: entry.value.toYaml(),
},
},
}.sortOnKey();
}
String toYamlString() => yamlEncode(toYaml());
@override
bool operator ==(Object other) {
if (other is! BuildConfig) {
return false;
}
if (other.outDir != outDir) return false;
if (other.packageName != packageName) return false;
if (other.packageRoot != packageRoot) return false;
if (other.dryRun != dryRun) return false;
if (other.targetOs != targetOs) return false;
if (other.linkModePreference != linkModePreference) return false;
if (!dryRun) {
if (other.buildMode != buildMode) return false;
if (other.targetArchitecture != targetArchitecture) return false;
if (other.targetIOSSdk != targetIOSSdk) return false;
if (other.targetAndroidNdkApi != targetAndroidNdkApi) return false;
if (other.cCompiler != cCompiler) return false;
if (!const DeepCollectionEquality()
.equals(other.dependencyMetadata, _dependencyMetadata)) return false;
}
return true;
}
@override
int get hashCode => Object.hashAll([
outDir,
packageName,
packageRoot,
targetOs,
linkModePreference,
dryRun,
if (!dryRun) ...[
buildMode,
const DeepCollectionEquality().hash(dependencyMetadata),
targetArchitecture,
targetIOSSdk,
targetAndroidNdkApi,
cCompiler,
],
]);
@override
String toString() => 'BuildConfig(${toYaml()})';
void _ensureNotDryRun() {
if (dryRun) {
throw StateError('''This field is not available in dry runs.
In Flutter projects, native builds are generated per OS which target multiple
architectures, build modes, etc. Therefore, the list of native assets produced
can _only_ depend on OS.''');
}
}
void _throwIfNotNullInDryRun<T>(String key) {
final object = config.valueOf<T?>(key);
if (object != null) {
throw const FormatException('''This field is not available in dry runs.
In Flutter projects, native builds are generated per OS which target multiple
architectures, build modes, etc. Therefore, the list of native assets produced
can _only_ depend on OS.''');
}
}
}
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 (!const ListEquality<String>()
.equals(other.envScriptArgs, envScriptArgs)) {
return false;
}
return true;
}
@override
int get hashCode => Object.hash(
_ar,
_cc,
_ld,
_envScript,
const ListEquality<String>().hash(envScriptArgs),
);
}