blob: 86b6d3d12caa927aa8fd4b0e09b9c30be0557d15 [file] [log] [blame] [edit]
// Copyright (c) 2024, 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:hooks/hooks.dart';
import 'code_asset.dart';
import 'config.dart';
import 'link_mode.dart';
import 'link_mode_preference.dart';
import 'os.dart';
import 'syntax.g.dart';
/// Validates the code asset specific parts of a [BuildInput].
Future<ValidationErrors> validateCodeAssetBuildInput(BuildInput input) async =>
[
..._validateConfig('BuildInput.config.code', input.config),
...await _validateCodeAssetHookInput([
for (final assets in input.assets.encodedAssets.values) ...assets,
]),
];
/// Validates the code asset specific parts of a [LinkInput].
Future<ValidationErrors> validateCodeAssetLinkInput(LinkInput input) async => [
..._validateConfig('LinkInput.config.code', input.config),
...await _validateCodeAssetHookInput(input.assets.encodedAssets),
];
ValidationErrors _validateConfig(String inputName, HookConfig config) {
final syntaxErrors = _validateConfigSyntax(config);
if (syntaxErrors.isNotEmpty) {
return syntaxErrors;
}
final code = config.code;
final errors = <String>[];
final cCompiler = code.cCompiler;
if (cCompiler != null) {
errors.addAll([
..._validateFile('$inputName.cCompiler.compiler', cCompiler.compiler),
..._validateFile('$inputName.cCompiler.linker', cCompiler.linker),
..._validateFile('$inputName.cCompiler.archiver', cCompiler.archiver),
]);
if (code.targetOS == OS.windows &&
cCompiler.windows.developerCommandPrompt != null) {
errors.addAll([
..._validateFile(
'$inputName.cCompiler.windows.developerCommandPrompt.script',
cCompiler.windows.developerCommandPrompt!.script,
),
]);
}
}
return errors;
}
ValidationErrors _validateConfigSyntax(HookConfig config) {
final syntaxNode = ConfigSyntax.fromJson(config.json, path: config.path);
final syntaxErrors = syntaxNode.validate();
if (syntaxErrors.isEmpty) {
return [];
}
return [...syntaxErrors, _semanticValidationSkippedMessage(syntaxNode.path)];
}
Future<ValidationErrors> _validateCodeAssetHookInput(
List<EncodedAsset> encodedAssets,
) async {
final errors = <String>[];
for (final asset in encodedAssets) {
if (!asset.isCodeAsset) continue;
final syntaxErrors = _validateCodeAssetSyntax(asset);
if (syntaxErrors.isNotEmpty) {
errors.addAll(syntaxErrors);
continue;
}
errors.addAll(_validateCodeAssetFile(CodeAsset.fromEncoded(asset)));
}
return errors;
}
/// Validates the code asset specific parts of a [BuildOutput].
Future<ValidationErrors> validateCodeAssetBuildOutput(
BuildInput input,
BuildOutput output,
) => _validateCodeAssetBuildOrLinkOutput(
input,
input.config.code,
output.assets.encodedAssets,
[
...output.assets.encodedAssetsForBuild,
...output.assets.encodedAssetsForLinking.values.expand((assets) => assets),
],
output,
true,
);
/// Validates the code asset specific parts of a [LinkOutput].
Future<ValidationErrors> validateCodeAssetLinkOutput(
LinkInput input,
LinkOutput output,
) => _validateCodeAssetBuildOrLinkOutput(
input,
input.config.code,
output.assets.encodedAssets,
output.assets.encodedAssetsForLink.values.expand((assets) => assets),
output,
false,
);
/// Validates that the given code assets can be used together in an application.
///
/// Some restrictions - e.g. unique shared library names - have to be validated
/// on the entire application build and not on individual `hook/build.dart`
/// invocations.
Future<ValidationErrors> validateCodeAssetInApplication(
List<EncodedAsset> assets,
) async {
final codeAssets = <CodeAsset>[];
for (final asset in assets) {
if (!asset.isCodeAsset) continue;
final codeAsset = CodeAsset.fromEncoded(asset);
codeAssets.add(codeAsset);
}
final errors = <String>[];
_validateNoDuplicateDylibNames(errors, codeAssets);
_validateNoDuplicateAssetIds(errors, codeAssets);
return errors;
}
Future<ValidationErrors> _validateCodeAssetBuildOrLinkOutput(
HookInput input,
CodeConfig codeConfig,
Iterable<EncodedAsset> encodedAssetsBundled,
Iterable<EncodedAsset> encodedAssetsNotBundled,
HookOutput output,
bool isBuild,
) async {
final errors = <String>[];
final codeAssetsBundled = <CodeAsset>[];
for (final asset in encodedAssetsBundled) {
if (!asset.isCodeAsset) continue;
final syntaxErrors = _validateCodeAssetSyntax(asset);
if (syntaxErrors.isNotEmpty) {
errors.addAll(syntaxErrors);
continue;
}
final codeAsset = CodeAsset.fromEncoded(asset);
_validateCodeAsset(input, codeConfig, codeAsset, errors, isBuild, true);
codeAssetsBundled.add(codeAsset);
}
_validateNoDuplicateDylibNames(errors, codeAssetsBundled);
_validateNoDuplicateAssetIds(errors, codeAssetsBundled);
final codeAssetsNotBundled = <CodeAsset>[];
for (final asset in encodedAssetsNotBundled) {
if (!asset.isCodeAsset) continue;
final syntaxErrors = _validateCodeAssetSyntax(asset);
if (syntaxErrors.isNotEmpty) {
errors.addAll(syntaxErrors);
continue;
}
final codeAsset = CodeAsset.fromEncoded(asset);
_validateCodeAsset(input, codeConfig, codeAsset, errors, isBuild, false);
codeAssetsNotBundled.add(codeAsset);
}
_validateNoDuplicateDylibNames(errors, codeAssetsNotBundled);
_validateNoDuplicateAssetIds(errors, codeAssetsNotBundled);
return errors;
}
ValidationErrors _validateCodeAssetSyntax(EncodedAsset encodedAsset) {
if (!encodedAsset.isCodeAsset) {
return [];
}
final syntaxNode = NativeCodeAssetEncodingSyntax.fromJson(
encodedAsset.encoding,
path: encodedAsset.encodingJsonPath ?? [],
);
final syntaxErrors = syntaxNode.validate();
if (syntaxErrors.isEmpty) {
return [];
}
return [...syntaxErrors, _semanticValidationSkippedMessage(syntaxNode.path)];
}
String _semanticValidationSkippedMessage(List<Object> jsonPath) {
final pathString = jsonPath.join('.');
return "Syntax errors in '$pathString'. Semantic validation skipped.";
}
void _validateCodeAsset(
HookInput input,
CodeConfig codeConfig,
CodeAsset codeAsset,
ValidationErrors errors,
bool validateAssetId,
bool validateLinkMode,
) {
final id = codeAsset.id;
final prefix = 'package:${input.packageName}/';
if (validateAssetId && !id.startsWith(prefix)) {
errors.add('Code asset "$id" does not start with "$prefix".');
}
if (validateLinkMode) {
final preference = codeConfig.linkModePreference;
final linkMode = codeAsset.linkMode;
if ((linkMode is DynamicLoading &&
preference == LinkModePreference.static) ||
(linkMode is StaticLinking &&
preference == LinkModePreference.dynamic)) {
errors.add(
'CodeAsset "$id" has a link mode "$linkMode", which '
'is not allowed by by the input link mode preference '
'"$preference".',
);
}
}
errors.addAll(_validateCodeAssetFile(codeAsset));
}
ValidationErrors _validateCodeAssetFile(CodeAsset codeAsset) {
final id = codeAsset.id;
final file = codeAsset.file;
return [
if (file == null && _mustHaveFile(codeAsset.linkMode))
'CodeAsset "$id" has no file.',
if (file != null) ..._validateFile('Code asset "$id" file', file),
];
}
bool _mustHaveFile(LinkMode linkMode) => switch (linkMode) {
LookupInExecutable _ => false,
LookupInProcess _ => false,
DynamicLoadingSystem _ => false,
DynamicLoadingBundled _ => true,
StaticLinking _ => true,
_ => throw UnsupportedError('Unknown link mode: $linkMode.'),
};
void _validateNoDuplicateDylibNames(
ValidationErrors errors,
List<CodeAsset> codeAssets,
) {
final fileNameToAssets = <String, Set<CodeAsset>>{};
for (final codeAsset in codeAssets) {
final file = codeAsset.file;
if (file != null) {
final fileName = file.pathSegments.where((s) => s.isNotEmpty).last;
fileNameToAssets[fileName] ??= {};
fileNameToAssets[fileName]!.add(codeAsset);
}
}
for (final fileName in fileNameToAssets.keys) {
final assets = fileNameToAssets[fileName]!;
if (assets.length > 1) {
final assetIdsString = assets.map((e) => '"${e.id}"').join(', ');
final error =
'Duplicate dynamic library file name "$fileName" for the following'
' asset ids: $assetIdsString.';
errors.add(error);
}
}
}
void _validateNoDuplicateAssetIds(
ValidationErrors errors,
List<CodeAsset> codeAssets,
) {
final assetIdToAssets = <String, Set<CodeAsset>>{};
for (final codeAsset in codeAssets) {
final assetId = codeAsset.id;
assetIdToAssets[assetId] ??= {};
assetIdToAssets[assetId]!.add(codeAsset);
}
for (final assetIds in assetIdToAssets.values) {
if (assetIds.length > 1) {
final assetId = assetIds.first.id;
final filesString = assetIds.map((e) => '"${e.file?.path}"').join(', ');
final error =
'Multiple assets with the same id: "$assetId". '
'The duplicate assets have the following files: $filesString.';
errors.add(error);
}
}
}
ValidationErrors _validateFile(
String name,
Uri uri, {
bool mustExist = true,
bool mustBeAbsolute = true,
}) {
final errors = <String>[];
if (mustBeAbsolute && !uri.isAbsolute) {
errors.add('$name (${uri.toFilePath()}) must be an absolute path.');
}
if (mustExist && !File.fromUri(uri).existsSync()) {
errors.add('$name (${uri.toFilePath()}) does not exist as a file.');
}
return errors;
}