| // 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 'package:logging/logging.dart'; | |
| import 'code_asset.dart'; | |
| import 'config.dart'; | |
| import 'link_mode.dart'; | |
| import 'link_mode_preference.dart'; | |
| import 'native_library_validation.dart'; | |
| import 'os.dart'; | |
| import 'syntax.g.dart'; | |
| /// Validates the code asset specific parts of a [BuildInput]. | |
| Future<ValidationErrors> validateCodeAssetBuildInput(BuildInput input) async { | |
| if (!input.config.buildCodeAssets) { | |
| return []; | |
| } | |
| return [ | |
| ..._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 { | |
| if (!input.config.buildCodeAssets) { | |
| return []; | |
| } | |
| return [ | |
| ..._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, { | |
| Logger? logger, | |
| }) async { | |
| if (!input.config.buildCodeAssets) { | |
| return []; | |
| } | |
| return _validateCodeAssetBuildOrLinkOutput( | |
| input, | |
| input.config.code, | |
| output.assets.encodedAssets, | |
| [ | |
| ...output.assets.encodedAssetsForBuild, | |
| ...output.assets.encodedAssetsForLinking.values.expand( | |
| (assets) => assets, | |
| ), | |
| ], | |
| output, | |
| true, | |
| logger, | |
| ); | |
| } | |
| /// Validates the code asset specific parts of a [LinkOutput]. | |
| Future<ValidationErrors> validateCodeAssetLinkOutput( | |
| LinkInput input, | |
| LinkOutput output, { | |
| Logger? logger, | |
| }) async { | |
| if (!input.config.buildCodeAssets) { | |
| return []; | |
| } | |
| return _validateCodeAssetBuildOrLinkOutput( | |
| input, | |
| input.config.code, | |
| output.assets.encodedAssets, | |
| output.assets.encodedAssetsForLink.values.expand((assets) => assets), | |
| output, | |
| false, | |
| logger, | |
| ); | |
| } | |
| /// 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, | |
| Logger? logger, | |
| ) 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); | |
| await _validateCodeAsset( | |
| input, | |
| codeConfig, | |
| codeAsset, | |
| errors, | |
| isBuild, | |
| true, | |
| logger: logger, | |
| ); | |
| 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); | |
| await _validateCodeAsset( | |
| input, | |
| codeConfig, | |
| codeAsset, | |
| errors, | |
| isBuild, | |
| false, | |
| logger: logger, | |
| ); | |
| 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."; | |
| } | |
| Future<void> _validateCodeAsset( | |
| HookInput input, | |
| CodeConfig codeConfig, | |
| CodeAsset codeAsset, | |
| ValidationErrors errors, | |
| bool validateAssetId, | |
| bool validateLinkMode, { | |
| Logger? logger, | |
| }) async { | |
| 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)); | |
| await _validateCodeAssetArchitecture(codeConfig, codeAsset, errors, logger); | |
| } | |
| /// Checks that a bundled dynamic library was built for the target | |
| /// architecture by reading its file header. | |
| /// | |
| /// A file whose format or machine value is not recognized only produces a | |
| /// warning on [logger]: the goal is to catch mixed-up architectures early | |
| /// with a clear message, not to reject libraries this check does not know. | |
| Future<void> _validateCodeAssetArchitecture( | |
| CodeConfig codeConfig, | |
| CodeAsset codeAsset, | |
| ValidationErrors errors, | |
| Logger? logger, | |
| ) async { | |
| final fileUri = codeAsset.file; | |
| if (codeAsset.linkMode is! DynamicLoadingBundled || fileUri == null) { | |
| return; | |
| } | |
| final file = File.fromUri(fileUri); | |
| if (!file.existsSync()) return; | |
| final id = codeAsset.id; | |
| final result = await validateNativeLibrary( | |
| NativeLibraryValidationContext(file: fileUri, config: codeConfig), | |
| ); | |
| for (final error in result.errors) { | |
| errors.add('Code asset "$id" file "${file.path}" $error'); | |
| } | |
| if (result.warnings.isNotEmpty) { | |
| logger?.warning( | |
| 'Could not validate code asset "$id" file "${file.path}": ' | |
| '${result.warnings.join(' ')} If this is a valid native library, please ' | |
| 'file an issue at https://github.com/dart-lang/native/issues.', | |
| ); | |
| } | |
| } | |
| 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, | |
| }; | |
| 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. ' | |
| 'Prefer constructing it via `input.outputDirectoryShared`.', | |
| ); | |
| } | |
| if (mustExist && !File.fromUri(uri).existsSync()) { | |
| errors.add('$name (${uri.toFilePath()}) does not exist as a file.'); | |
| } | |
| return errors; | |
| } |