diff --git a/.github/workflows/c_compiler.yaml b/.github/workflows/c_compiler.yaml index ccd23a7937..1473cd3961 100644 --- a/.github/workflows/c_compiler.yaml +++ b/.github/workflows/c_compiler.yaml @@ -32,6 +32,9 @@ jobs: - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{matrix.sdk}} + - uses: nttld/setup-ndk@deccd078bf3db957dbdee9862f51955b35ac81dd + with: + ndk-version: r25b - run: dart pub get @@ -40,6 +43,9 @@ jobs: - run: dart format --output=none --set-exit-if-changed . if: ${{matrix.run-tests}} + - name: Install native toolchains + run: sudo apt-get install clang-14 gcc-i686-linux-gnu gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf + - run: dart test if: ${{matrix.run-tests}} diff --git a/.github/workflows/native_assets_cli.yaml b/.github/workflows/native_assets_cli.yaml index 34d2520a1b..30c4d0c8bc 100644 --- a/.github/workflows/native_assets_cli.yaml +++ b/.github/workflows/native_assets_cli.yaml @@ -32,14 +32,22 @@ jobs: - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: ${{matrix.sdk}} + - uses: nttld/setup-ndk@deccd078bf3db957dbdee9862f51955b35ac81dd + with: + ndk-version: r25b - run: dart pub get + - run: dart pub get + working-directory: pkgs/native_assets_cli/example/native_add/ - run: dart analyze --fatal-infos - run: dart format --output=none --set-exit-if-changed . if: ${{matrix.run-tests}} + - name: Install native toolchains + run: sudo apt-get install clang-14 gcc-i686-linux-gnu gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf + - run: dart test if: ${{matrix.run-tests}} diff --git a/pkgs/c_compiler/README.md b/pkgs/c_compiler/README.md index d8c8f7e619..3327ce6766 100644 --- a/pkgs/c_compiler/README.md +++ b/pkgs/c_compiler/README.md @@ -1,4 +1,4 @@ -[![package:c_compiler](https://github.com/dart-lang/native/actions/workflows/c_compiler.yml/badge.svg)](https://github.com/dart-lang/native/actions/workflows/c_compiler.yml) +[![package:c_compiler](https://github.com/dart-lang/native/actions/workflows/c_compiler.yaml/badge.svg)](https://github.com/dart-lang/native/actions/workflows/c_compiler.yaml) [![pub package](https://img.shields.io/pub/v/c_compiler.svg)](https://pub.dev/packages/c_compiler) [![Coverage Status](https://coveralls.io/repos/github/dart-lang/native/badge.svg?branch=main)](https://coveralls.io/github/dart-lang/tools?branch=main) diff --git a/pkgs/c_compiler/lib/c_compiler.dart b/pkgs/c_compiler/lib/c_compiler.dart index dcdff2381d..8ef513d5d4 100644 --- a/pkgs/c_compiler/lib/c_compiler.dart +++ b/pkgs/c_compiler/lib/c_compiler.dart @@ -5,4 +5,11 @@ /// A library to invoke the native C compiler installed on the host machine. library; -export 'src/c_compiler_base.dart'; +export 'src/cbuilder/cbuilder.dart'; +export 'src/native_toolchain/android_ndk.dart'; +export 'src/native_toolchain/clang.dart'; +export 'src/native_toolchain/gcc.dart'; +export 'src/tool/tool.dart'; +export 'src/tool/tool_instance.dart'; +export 'src/tool/tool_requirement.dart'; +export 'src/tool/tool_resolver.dart'; diff --git a/pkgs/c_compiler/lib/src/cbuilder/cbuilder.dart b/pkgs/c_compiler/lib/src/cbuilder/cbuilder.dart new file mode 100644 index 0000000000..a0c5a6e2b1 --- /dev/null +++ b/pkgs/c_compiler/lib/src/cbuilder/cbuilder.dart @@ -0,0 +1,124 @@ +// 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:io'; + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import 'run_cbuilder.dart'; + +abstract class Builder { + Future run({ + required BuildConfig buildConfig, + required BuildOutput buildOutput, + Logger? logger, + }); +} + +/// Specification for building an artifact with a C compiler. +class CBuilder implements Builder { + /// What kind of artifact to build. + final _CBuilderType _type; + + /// Name of the library or executable to build. + /// + /// The filename will be decided by [BuildConfig.target] and + /// [OS.libraryFileName] or [OS.executableFileName]. + /// + /// File will be placed in [BuildConfig.outDir]. + final String name; + + /// Asset identifier. + /// + /// Used to output the [BuildOutput.assets]. + /// + /// If omitted, no asset will be added to the build output. + final String? assetName; + + /// Sources to build the library or executable. + /// + /// Resolved against [BuildConfig.packageRoot]. + /// + /// Used to output the [BuildOutput.dependencies]. + final List sources; + + /// The dart files involved in building this artifact. + /// + /// Resolved against [BuildConfig.packageRoot]. + /// + /// Used to output the [BuildOutput.dependencies]. + final List dartBuildFiles; + + CBuilder.library({ + required this.name, + required this.assetName, + this.sources = const [], + this.dartBuildFiles = const ['build.dart'], + }) : _type = _CBuilderType.library; + + CBuilder.executable({ + required this.name, + this.sources = const [], + this.dartBuildFiles = const ['build.dart'], + }) : _type = _CBuilderType.executable, + assetName = null; + + @override + Future run({ + required BuildConfig buildConfig, + required BuildOutput buildOutput, + Logger? logger, + }) async { + logger ??= Logger(''); + final outDir = buildConfig.outDir; + final packageRoot = buildConfig.packageRoot; + await Directory.fromUri(outDir).create(recursive: true); + final packaging = buildConfig.packaging.preferredPackaging.first; + final libUri = + outDir.resolve(buildConfig.target.os.libraryFileName(name, packaging)); + final exeUri = + outDir.resolve(buildConfig.target.os.executableFileName(name)); + final sources = [ + for (final source in this.sources) packageRoot.resolve(source), + ]; + final dartBuildFiles = [ + for (final source in this.dartBuildFiles) packageRoot.resolve(source), + ]; + + final task = RunCBuilder( + buildConfig: buildConfig, + logger: logger, + sources: sources, + dynamicLibrary: + _type == _CBuilderType.library && packaging == Packaging.dynamic + ? libUri + : null, + staticLibrary: + _type == _CBuilderType.library && packaging == Packaging.static + ? libUri + : null, + executable: _type == _CBuilderType.executable ? exeUri : null, + ); + await task.run(); + + if (assetName != null) { + buildOutput.assets.add(Asset( + name: assetName!, + packaging: packaging, + target: buildConfig.target, + path: AssetAbsolutePath(libUri), + )); + } + buildOutput.dependencies.dependencies.addAll([ + ...sources, + ...dartBuildFiles, + ]); + } +} + +enum _CBuilderType { + executable, + library, +} diff --git a/pkgs/c_compiler/lib/src/cbuilder/compiler_resolver.dart b/pkgs/c_compiler/lib/src/cbuilder/compiler_resolver.dart new file mode 100644 index 0000000000..ddbfb1f0f1 --- /dev/null +++ b/pkgs/c_compiler/lib/src/cbuilder/compiler_resolver.dart @@ -0,0 +1,154 @@ +// 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:io'; + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../native_toolchain/android_ndk.dart'; +import '../native_toolchain/clang.dart'; +import '../native_toolchain/gcc.dart'; +import '../native_toolchain/recognizer.dart'; +import '../tool/tool.dart'; +import '../tool/tool_error.dart'; +import '../tool/tool_instance.dart'; + +// TODO(dacoharkes): This should support alternatives. +// For example use Clang or MSVC on Windows. +class CompilerResolver { + final BuildConfig buildConfig; + final Logger? logger; + final Target host; + + CompilerResolver({ + required this.buildConfig, + required this.logger, + Target? host, // Only visible for testing. + }) : host = host ?? Target.current; + + Future resolveCompiler() async { + // First, check if the launcher provided a direct path to the compiler. + var result = await _tryLoadCompilerFromConfig( + BuildConfig.ccConfigKey, + (buildConfig) => buildConfig.cc, + ); + + // Then, try to detect on the host machine. + final tool = _selectCompiler(); + if (tool != null) { + result ??= await _tryLoadToolFromNativeToolchain(tool); + } + + if (result != null) { + return result; + } + + final target = buildConfig.target; + final errorMessage = + "No tools configured on host '$host' with target '$target'."; + logger?.severe(errorMessage); + throw ToolError(errorMessage); + } + + /// Select the right compiler for cross compiling to the specified target. + Tool? _selectCompiler() { + final target = buildConfig.target; + + if (target == host) return clang; + if (target.os == OS.android) return androidNdkClang; + if (host.os == OS.linux) { + switch (target) { + case Target.linuxArm: + return armLinuxGnueabihfGcc; + case Target.linuxArm64: + return aarch64LinuxGnuGcc; + case Target.linuxIA32: + return i686LinuxGnuGcc; + } + } + + return null; + } + + Future _tryLoadCompilerFromConfig( + String configKey, Uri? Function(BuildConfig) getter) async { + final configCcUri = getter(buildConfig); + if (configCcUri != null) { + assert(await File.fromUri(configCcUri).exists()); + logger?.finer('Using compiler ${configCcUri.path} ' + 'from config[${BuildConfig.ccConfigKey}].'); + return (await CompilerRecognizer(configCcUri).resolve(logger: logger)) + .first; + } + logger?.finer('No compiler set in config[${BuildConfig.ccConfigKey}].'); + return null; + } + + Future _tryLoadToolFromNativeToolchain(Tool tool) async { + final resolved = (await tool.defaultResolver!.resolve(logger: logger)) + .where((i) => i.tool == tool) + .toList() + ..sort(); + return resolved.isEmpty ? null : resolved.first; + } + + Future resolveArchiver() async { + // First, check if the launcher provided a direct path to the compiler. + var result = await _tryLoadArchiverFromConfig( + BuildConfig.arConfigKey, + (buildConfig) => buildConfig.ar, + ); + + // Then, try to detect on the host machine. + final tool = _selectArchiver(); + if (tool != null) { + result ??= await _tryLoadToolFromNativeToolchain(tool); + } + + if (result != null) { + return result; + } + + final target = buildConfig.target; + final errorMessage = + "No tools configured on host '$host' with target '$target'."; + logger?.severe(errorMessage); + throw ToolError(errorMessage); + } + + /// Select the right archiver for cross compiling to the specified target. + Tool? _selectArchiver() { + final target = buildConfig.target; + + if (target == host) return llvmAr; + if (target.os == OS.android) return androidNdkLlvmAr; + if (host.os == OS.linux) { + switch (target) { + case Target.linuxArm: + return armLinuxGnueabihfGccAr; + case Target.linuxArm64: + return aarch64LinuxGnuGccAr; + case Target.linuxIA32: + return i686LinuxGnuGccAr; + } + } + + return null; + } + + Future _tryLoadArchiverFromConfig( + String configKey, Uri? Function(BuildConfig) getter) async { + final configArUri = getter(buildConfig); + if (configArUri != null) { + assert(await File.fromUri(configArUri).exists()); + logger?.finer('Using archiver ${configArUri.path} ' + 'from config[${BuildConfig.arConfigKey}].'); + return (await ArchiverRecognizer(configArUri).resolve(logger: logger)) + .first; + } + logger?.finer('No archiver set in config[${BuildConfig.arConfigKey}].'); + return null; + } +} diff --git a/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart b/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart new file mode 100644 index 0000000000..cdef265e0e --- /dev/null +++ b/pkgs/c_compiler/lib/src/cbuilder/run_cbuilder.dart @@ -0,0 +1,102 @@ +// 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 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../utils/run_process.dart'; +import 'compiler_resolver.dart'; + +class RunCBuilder { + final BuildConfig buildConfig; + final Logger logger; + final List sources; + final Uri? executable; + final Uri? dynamicLibrary; + final Uri? staticLibrary; + final Uri outDir; + final Target target; + + RunCBuilder({ + required this.buildConfig, + required this.logger, + this.sources = const [], + this.executable, + this.dynamicLibrary, + this.staticLibrary, + }) : outDir = buildConfig.outDir, + target = buildConfig.target, + assert([executable, dynamicLibrary, staticLibrary] + .whereType() + .length == + 1); + + Future compiler() async { + final resolver = CompilerResolver(buildConfig: buildConfig, logger: logger); + return (await resolver.resolveCompiler()).uri; + } + + Future archiver() async { + final resolver = CompilerResolver(buildConfig: buildConfig, logger: logger); + return (await resolver.resolveArchiver()).uri; + } + + Future run() async { + final compiler_ = await compiler(); + final isStaticLib = staticLibrary != null; + Uri? archiver_; + if (isStaticLib) { + archiver_ = await archiver(); + } + + await runProcess( + executable: compiler_, + arguments: [ + if (target.os == OS.android) ...[ + // TODO(dacoharkes): How to solve linking issues? + // Non-working fix: --sysroot=$NDKPATH/toolchains/llvm/prebuilt/linux-x86_64/sysroot. + // The sysroot should be discovered automatically after NDK 22. + // Workaround: + if (dynamicLibrary != null) '-nostartfiles', + '--target=${androidNdkClangTargetFlags[target]!}', + ], + ...sources.map((e) => e.path), + if (executable != null) ...[ + '-o', + outDir.resolveUri(executable!).path, + ], + if (dynamicLibrary != null) ...[ + '--shared', + '-o', + outDir.resolveUri(dynamicLibrary!).path, + ] else if (staticLibrary != null) ...[ + '-c', + '-o', + outDir.resolve('out.o').path, + ], + ], + logger: logger, + captureOutput: false, + ); + if (staticLibrary != null) { + await runProcess( + executable: archiver_!, + arguments: [ + 'rc', + outDir.resolveUri(staticLibrary!).path, + outDir.resolve('out.o').path, + ], + logger: logger, + captureOutput: false, + ); + } + } + + static const androidNdkClangTargetFlags = { + Target.androidArm: 'armv7a-linux-androideabi', + Target.androidArm64: 'aarch64-linux-android', + Target.androidIA32: 'i686-linux-android', + Target.androidX64: 'x86_64-linux-android', + }; +} diff --git a/pkgs/c_compiler/lib/src/native_toolchain/android_ndk.dart b/pkgs/c_compiler/lib/src/native_toolchain/android_ndk.dart new file mode 100644 index 0000000000..40ad71a0db --- /dev/null +++ b/pkgs/c_compiler/lib/src/native_toolchain/android_ndk.dart @@ -0,0 +1,103 @@ +// 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:io'; + +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +import '../tool/tool.dart'; +import '../tool/tool_instance.dart'; +import '../tool/tool_resolver.dart'; +import 'clang.dart'; + +final androidNdk = Tool( + name: 'Android NDK', + defaultResolver: _AndroidNdkResolver(), +); + +/// [clang] with [Tool.defaultResolver] for the [OS.android] NDK. +final androidNdkClang = Tool( + name: clang.name, + defaultResolver: _AndroidNdkResolver(), +); + +/// [llvmAr] with [Tool.defaultResolver] for the [OS.android] NDK. +final androidNdkLlvmAr = Tool( + name: llvmAr.name, + defaultResolver: _AndroidNdkResolver(), +); + +/// [lld] with [Tool.defaultResolver] for the [OS.android] NDK. +final androidNdkLld = Tool( + name: lld.name, + defaultResolver: _AndroidNdkResolver(), +); + +class _AndroidNdkResolver implements ToolResolver { + final installLocationResolver = PathVersionResolver( + wrappedResolver: ToolResolvers([ + RelativeToolResolver( + toolName: 'Android NDK', + wrappedResolver: PathToolResolver(toolName: 'ndk-build'), + relativePath: Uri(path: '.'), + ), + InstallLocationResolver( + toolName: 'Android NDK', + paths: [ + if (Platform.isLinux) ...[ + '\$HOME/Android/Sdk/ndk/*/', + '\$HOME/Android/Sdk/ndk-bundle/', + ], + ], + ), + ]), + ); + + @override + Future> resolve({Logger? logger}) async { + final ndkInstances = await installLocationResolver.resolve(logger: logger); + + return [ + for (final ndkInstance in ndkInstances) ...[ + ndkInstance, + ...await tryResolveClang(ndkInstance) + ] + ]; + } + + Future> tryResolveClang( + ToolInstance androidNdkInstance) async { + final result = []; + final prebuiltUri = + androidNdkInstance.uri.resolve('toolchains/llvm/prebuilt/'); + final prebuiltDir = Directory.fromUri(prebuiltUri); + final hostArchDirs = + (await prebuiltDir.list().toList()).whereType().toList(); + for (final hostArchDir in hostArchDirs) { + final clangUri = hostArchDir.uri.resolve('bin/clang'); + if (await File.fromUri(clangUri).exists()) { + result.add(await CliVersionResolver.lookupVersion(ToolInstance( + tool: androidNdkClang, + uri: clangUri, + ))); + } + final arUri = hostArchDir.uri.resolve('bin/llvm-ar'); + if (await File.fromUri(arUri).exists()) { + result.add(await CliVersionResolver.lookupVersion(ToolInstance( + tool: androidNdkLlvmAr, + uri: arUri, + ))); + } + final ldUri = hostArchDir.uri.resolve('bin/ld.lld'); + if (await File.fromUri(arUri).exists()) { + result.add(await CliVersionResolver.lookupVersion(ToolInstance( + tool: androidNdkLld, + uri: ldUri, + ))); + } + } + return result; + } +} diff --git a/pkgs/c_compiler/lib/src/native_toolchain/clang.dart b/pkgs/c_compiler/lib/src/native_toolchain/clang.dart new file mode 100644 index 0000000000..d1e8845763 --- /dev/null +++ b/pkgs/c_compiler/lib/src/native_toolchain/clang.dart @@ -0,0 +1,50 @@ +// 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 '../tool/tool.dart'; +import '../tool/tool_resolver.dart'; + +/// The Clang compiler. +/// +/// https://clang.llvm.org/ +final Tool clang = Tool( + name: 'Clang', + defaultResolver: CliVersionResolver( + wrappedResolver: ToolResolvers([ + PathToolResolver(toolName: 'Clang'), + ]), + ), +); + +/// The LLVM archiver. +/// +/// https://llvm.org/docs/CommandGuide/llvm-ar.html +final Tool llvmAr = Tool( + name: 'LLVM archiver', + defaultResolver: CliVersionResolver( + wrappedResolver: ToolResolvers([ + RelativeToolResolver( + toolName: 'LLVM archiver', + wrappedResolver: clang.defaultResolver!, + relativePath: Uri.file('llvm-ar'), + ), + ]), + ), +); + +/// The LLVM Linker. +/// +/// https://lld.llvm.org/ +final Tool lld = Tool( + name: 'LLD', + defaultResolver: CliVersionResolver( + wrappedResolver: ToolResolvers([ + RelativeToolResolver( + toolName: 'LLD', + wrappedResolver: clang.defaultResolver!, + relativePath: Uri.file('ld.lld'), + ), + ]), + ), +); diff --git a/pkgs/c_compiler/lib/src/native_toolchain/gcc.dart b/pkgs/c_compiler/lib/src/native_toolchain/gcc.dart new file mode 100644 index 0000000000..0f2477b155 --- /dev/null +++ b/pkgs/c_compiler/lib/src/native_toolchain/gcc.dart @@ -0,0 +1,82 @@ +// 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 'package:native_assets_cli/native_assets_cli.dart'; + +import '../tool/tool.dart'; +import '../tool/tool_resolver.dart'; + +/// The GNU Compiler Collection. +/// +/// https://gcc.gnu.org/ +final gcc = Tool(name: 'GCC'); + +/// The GNU GCC archiver. +final gnuArchiver = Tool(name: 'GNU archiver'); + +/// The GNU linker. +/// +/// https://ftp.gnu.org/old-gnu/Manuals/ld-2.9.1/ld.html +final gnuLinker = Tool(name: 'GNU linker'); + +/// [gcc] with [Tool.defaultResolver] for [Architecture.ia32]. +final i686LinuxGnuGcc = _gcc('i686-linux-gnu'); + +/// [gnuArchiver] with [Tool.defaultResolver] for [Architecture.ia32]. +final i686LinuxGnuGccAr = _gnuArchiver('i686-linux-gnu'); + +/// [gnuLinker] with [Tool.defaultResolver] for [Architecture.ia32]. +final i686LinuxGnuLd = _gnuLinker('i686-linux-gnu'); + +/// [gcc] with [Tool.defaultResolver] for [Architecture.arm]. +final armLinuxGnueabihfGcc = _gcc('arm-linux-gnueabihf'); + +/// [gnuArchiver] with [Tool.defaultResolver] for [Architecture.arm]. +final armLinuxGnueabihfGccAr = _gnuArchiver('arm-linux-gnueabihf'); + +/// [gnuLinker] with [Tool.defaultResolver] for [Architecture.arm]. +final armLinuxGnueabihfLd = _gnuLinker('arm-linux-gnueabihf'); + +/// [gcc] with [Tool.defaultResolver] for [Architecture.arm64]. +final aarch64LinuxGnuGcc = _gcc('aarch64-linux-gnu'); + +/// [gnuArchiver] with [Tool.defaultResolver] for [Architecture.arm64]. +final aarch64LinuxGnuGccAr = _gnuArchiver('aarch64-linux-gnu'); + +/// [gnuLinker] with [Tool.defaultResolver] for [Architecture.arm64]. +final aarch64LinuxGnuLd = _gnuLinker('aarch64-linux-gnu'); + +Tool _gcc(String prefix) => Tool( + name: gcc.name, + defaultResolver: CliVersionResolver( + wrappedResolver: PathToolResolver( + toolName: gcc.name, + executableName: '$prefix-gcc', + ), + ), + ); + +Tool _gnuArchiver(String prefix) { + final gcc = _gcc(prefix); + return Tool( + name: gnuArchiver.name, + defaultResolver: RelativeToolResolver( + toolName: gnuArchiver.name, + wrappedResolver: gcc.defaultResolver!, + relativePath: Uri.file('$prefix-gcc-ar'), + ), + ); +} + +Tool _gnuLinker(String prefix) { + final gcc = _gcc(prefix); + return Tool( + name: gnuLinker.name, + defaultResolver: RelativeToolResolver( + toolName: gnuLinker.name, + wrappedResolver: gcc.defaultResolver!, + relativePath: Uri.file('$prefix-ld'), + ), + ); +} diff --git a/pkgs/c_compiler/lib/src/native_toolchain/recognizer.dart b/pkgs/c_compiler/lib/src/native_toolchain/recognizer.dart new file mode 100644 index 0000000000..1c8dda974e --- /dev/null +++ b/pkgs/c_compiler/lib/src/native_toolchain/recognizer.dart @@ -0,0 +1,113 @@ +// 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 'package:logging/logging.dart'; + +import '../tool/tool.dart'; +import '../tool/tool_instance.dart'; +import '../tool/tool_resolver.dart'; +import 'clang.dart'; +import 'gcc.dart'; + +class CompilerRecognizer implements ToolResolver { + final Uri uri; + + CompilerRecognizer(this.uri); + + @override + Future> resolve({Logger? logger}) async { + logger?.finer('Trying to recognize $uri.'); + final filePath = uri.toFilePath(); + Tool? tool; + if (filePath.contains('-gcc')) { + tool = gcc; + } else if (filePath.contains('clang')) { + tool = clang; + } + + if (tool != null) { + logger?.fine('Tool instance $uri is likely $tool.'); + final toolInstance = ToolInstance(tool: tool, uri: uri); + return [ + await CliVersionResolver.lookupVersion( + toolInstance, + logger: logger, + ), + ]; + } + + logger?.severe('Tool instance $uri not recognized.'); + return []; + } +} + +class LinkerRecognizer implements ToolResolver { + final Uri uri; + + LinkerRecognizer(this.uri); + + @override + Future> resolve({Logger? logger}) async { + logger?.finer('Trying to recognize $uri.'); + final filePath = uri.toFilePath(); + Tool? tool; + if (filePath.contains('-ld')) { + tool = gnuLinker; + } else if (filePath.contains('ld.lld')) { + tool = lld; + } + + if (tool != null) { + logger?.fine('Tool instance $uri is likely $tool.'); + final toolInstance = ToolInstance(tool: tool, uri: uri); + if (tool == lld) { + return [ + await CliVersionResolver.lookupVersion( + toolInstance, + logger: logger, + ), + ]; + } + return [toolInstance]; + } + + logger?.severe('Tool instance $uri not recognized.'); + return []; + } +} + +class ArchiverRecognizer implements ToolResolver { + final Uri uri; + + ArchiverRecognizer(this.uri); + + @override + Future> resolve({Logger? logger}) async { + logger?.finer('Trying to recognize $uri.'); + final filePath = uri.toFilePath(); + Tool? tool; + if (filePath.contains('-gcc-ar')) { + tool = gnuArchiver; + } else if (filePath.contains('llvm-ar')) { + tool = llvmAr; + } + + if (tool != null) { + logger?.fine('Tool instance $uri is likely $tool.'); + final toolInstance = ToolInstance(tool: tool, uri: uri); + if (tool == llvmAr) { + return [ + await CliVersionResolver.lookupVersion( + toolInstance, + logger: logger, + ), + ]; + } + return [toolInstance]; + } + + logger?.severe('Tool instance $uri not recognized.'); + return []; + } +} diff --git a/pkgs/c_compiler/lib/src/tool/tool.dart b/pkgs/c_compiler/lib/src/tool/tool.dart new file mode 100644 index 0000000000..ff42f28559 --- /dev/null +++ b/pkgs/c_compiler/lib/src/tool/tool.dart @@ -0,0 +1,25 @@ +// 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 'tool_resolver.dart'; + +class Tool { + final String name; + + ToolResolver? defaultResolver; + + Tool({ + required this.name, + this.defaultResolver, + }); + + @override + bool operator ==(Object other) => other is Tool && name == other.name; + + @override + int get hashCode => Object.hash(name, 133709); + + @override + String toString() => 'Tool($name)'; +} diff --git a/pkgs/c_compiler/lib/src/tool/tool_error.dart b/pkgs/c_compiler/lib/src/tool/tool_error.dart new file mode 100644 index 0000000000..def9ce4467 --- /dev/null +++ b/pkgs/c_compiler/lib/src/tool/tool_error.dart @@ -0,0 +1,14 @@ +// 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. + +/// The operation could not be performed due to a configuration error on the +/// host system. +class ToolError extends Error { + final String message; + + ToolError(this.message); + + @override + String toString() => 'System not configured correctly: $message'; +} diff --git a/pkgs/c_compiler/lib/src/tool/tool_instance.dart b/pkgs/c_compiler/lib/src/tool/tool_instance.dart new file mode 100644 index 0000000000..87c42d537d --- /dev/null +++ b/pkgs/c_compiler/lib/src/tool/tool_instance.dart @@ -0,0 +1,74 @@ +// 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 'package:pub_semver/pub_semver.dart'; + +import 'tool.dart'; + +class ToolInstance implements Comparable { + /// The name of the tool. + final Tool tool; + + /// The path of the native tool on the system. + final Uri uri; + + /// The version of the native tool. + /// + /// Can be null if version is hard to determine. + final Version? version; + + ToolInstance({ + required this.tool, + required this.uri, + this.version, + }); + + ToolInstance copyWith({Uri? uri, Version? version}) => ToolInstance( + tool: tool, + uri: uri ?? this.uri, + version: version ?? this.version, + ); + + @override + String toString() => 'ToolInstance(${tool.name}, $version, $uri)'; + + /// Compares this tool instance to [other]. + /// + /// When used in sorting, orders [ToolInstance]s according to: + /// 1. [tool] name, alphabetically; then + /// 2. [version], newest first and preferring having a version; then + /// 3. [uri], alphabetically. + @override + int compareTo(ToolInstance other) { + final nameCompare = tool.name.compareTo(other.tool.name); + if (nameCompare != 0) { + return nameCompare; + } + final version = this.version; + final otherVersion = other.version; + if (version != null || otherVersion != null) { + if (version == null) { + return 1; + } + if (otherVersion == null) { + return -1; + } + final versionCompare = version.compareTo(otherVersion); + if (versionCompare != 0) { + return -versionCompare; + } + } + return uri.toFilePath().compareTo(other.uri.toFilePath()); + } + + @override + bool operator ==(Object other) => + other is ToolInstance && + tool == other.tool && + uri == other.uri && + version == other.version; + + @override + int get hashCode => Object.hash(tool, uri, version); +} diff --git a/pkgs/c_compiler/lib/src/tool/tool_requirement.dart b/pkgs/c_compiler/lib/src/tool/tool_requirement.dart new file mode 100644 index 0000000000..33dba8ba49 --- /dev/null +++ b/pkgs/c_compiler/lib/src/tool/tool_requirement.dart @@ -0,0 +1,92 @@ +// 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 'package:pub_semver/pub_semver.dart'; + +import 'tool.dart'; +import 'tool_instance.dart'; + +abstract class Requirement { + /// Tries to satisfy this requirement. + /// + /// If the requirement can be satisfied, returns the set of tools that + /// satisfy the requirement. + /// + /// Currently does not check that we only use a single version of a tool. + List? satisfy(List allAvailableTools); +} + +class ToolRequirement implements Requirement { + final Tool tool; + + final Version? minimumVersion; + + ToolRequirement( + this.tool, { + this.minimumVersion, + }); + + @override + String toString() => + 'ToolRequirement(${tool.name}, minimumVersion: $minimumVersion)'; + + @override + List? satisfy(List availableToolInstances) { + final candidates = []; + for (final instance in availableToolInstances) { + if (instance.tool == tool) { + final minimumVersion_ = minimumVersion; + if (minimumVersion_ == null) { + candidates.add(instance); + } else { + final version = instance.version; + if (version != null && version >= minimumVersion_) { + candidates.add(instance); + } + } + } + } + if (candidates.isEmpty) { + return null; + } + candidates.sort(); + return [candidates.first]; + } +} + +class RequireOne implements Requirement { + final List alternatives; + + RequireOne(this.alternatives); + + @override + List? satisfy(List allAvailableTools) { + for (final alternative in alternatives) { + final result = alternative.satisfy(allAvailableTools); + if (result != null) { + return result; + } + } + return null; + } +} + +class RequireAll implements Requirement { + final List requirements; + + RequireAll(this.requirements); + + @override + List? satisfy(List allAvailableTools) { + final result = []; + for (final requirement in requirements) { + final requirementResult = requirement.satisfy(allAvailableTools); + if (requirementResult == null) { + return null; + } + result.addAll(requirementResult); + } + return result; + } +} diff --git a/pkgs/c_compiler/lib/src/tool/tool_resolver.dart b/pkgs/c_compiler/lib/src/tool/tool_resolver.dart new file mode 100644 index 0000000000..a8dd87aa2d --- /dev/null +++ b/pkgs/c_compiler/lib/src/tool/tool_resolver.dart @@ -0,0 +1,249 @@ +// 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:glob/glob.dart'; +import 'package:glob/list_local_fs.dart'; +import 'package:logging/logging.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import '../utils/run_process.dart'; +import '../utils/sem_version.dart'; +import 'tool.dart'; +import 'tool_error.dart'; +import 'tool_instance.dart'; + +abstract class ToolResolver { + /// Resolves tools on the host system. + Future> resolve({Logger? logger}); +} + +/// Tries to resolve a tool on the `PATH`. +/// +/// Uses `which` (`where` on Windows) to resolve a tool. +class PathToolResolver extends ToolResolver { + /// The [Tool.name] of the [Tool] to find on the `PATH`. + final String toolName; + + final String executableName; + + PathToolResolver({ + required this.toolName, + String? executableName, + }) : executableName = executableName ?? + Target.current.os.executableFileName(toolName.toLowerCase()); + + @override + Future> resolve({Logger? logger}) async { + logger?.finer('Looking for $toolName on PATH.'); + final uri = await runWhich(logger: logger); + if (uri == null) { + logger?.fine('Did not find $toolName on PATH.'); + return []; + } + final toolInstances = [ + ToolInstance(tool: Tool(name: toolName), uri: uri), + ]; + logger?.fine('Found ${toolInstances.single}.'); + return toolInstances; + } + + static Uri get which => Uri.file(Platform.isWindows ? 'where' : 'which'); + + Future runWhich({Logger? logger}) async { + final process = await runProcess( + executable: which, + arguments: [executableName], + logger: logger, + ); + if (process.exitCode == 0) { + final file = File(LineSplitter.split(process.stdout).first); + final uri = File(await file.resolveSymbolicLinks()).uri; + return uri; + } + // The exit code for executable not being on the `PATH`. + assert(process.exitCode == 1); + return null; + } +} + +class CliVersionResolver implements ToolResolver { + ToolResolver wrappedResolver; + + CliVersionResolver({required this.wrappedResolver}); + + @override + Future> resolve({Logger? logger}) async { + final toolInstances = await wrappedResolver.resolve(logger: logger); + return [ + for (final toolInstance in toolInstances) + await lookupVersion(toolInstance, logger: logger) + ]; + } + + static Future lookupVersion( + ToolInstance toolInstance, { + Logger? logger, + }) async { + if (toolInstance.version != null) return toolInstance; + logger?.finer('Looking up version with --version for $toolInstance.'); + final version = await executableVersion(toolInstance.uri, logger: logger); + final result = toolInstance.copyWith(version: version); + logger?.fine('Found version for $result.'); + return result; + } + + static Future executableVersion( + Uri executable, { + String argument = '--version', + int expectedExitCode = 0, + Logger? logger, + }) async { + final process = await runProcess( + executable: executable, + arguments: [argument], + logger: logger, + ); + if (process.exitCode != expectedExitCode) { + final executablePath = executable.toFilePath(); + throw ToolError( + '`$executablePath $argument` returned unexpected exit code: ' + '${process.exitCode}.'); + } + return versionFromString(process.stdout)!; + } +} + +class PathVersionResolver implements ToolResolver { + ToolResolver wrappedResolver; + + PathVersionResolver({required this.wrappedResolver}); + + @override + Future> resolve({Logger? logger}) async { + final toolInstances = await wrappedResolver.resolve(logger: logger); + + return [ + for (final toolInstance in toolInstances) lookupVersion(toolInstance) + ]; + } + + static ToolInstance lookupVersion(ToolInstance toolInstance) { + if (toolInstance.version != null) { + return toolInstance; + } + return toolInstance.copyWith( + version: version(toolInstance.uri), + ); + } + + static Version? version(Uri uri) { + final versionString = uri.pathSegments.where((e) => e != '').last; + final version = versionFromString(versionString); + return version; + } +} + +/// A resolver which invokes all [resolvers] tools. +class ToolResolvers implements ToolResolver { + final List resolvers; + + ToolResolvers(this.resolvers); + + @override + Future> resolve({Logger? logger}) async => [ + for (final resolver in resolvers) + ...await resolver.resolve(logger: logger) + ]; +} + +class InstallLocationResolver implements ToolResolver { + final String toolName; + final List paths; + + InstallLocationResolver({ + required this.toolName, + required this.paths, + }); + + static const home = '\$HOME'; + + @override + Future> resolve({Logger? logger}) async { + logger?.finer('Looking for $toolName in $paths.'); + final resolvedPaths = [ + for (final path in paths) ...await tryResolvePath(path) + ]; + final toolInstances = [ + for (final uri in resolvedPaths) + ToolInstance(tool: Tool(name: toolName), uri: uri), + ]; + if (toolInstances.isNotEmpty) { + logger?.fine('Found $toolInstances.'); + } else { + logger?.finer('Found no $toolName in $paths.'); + } + return toolInstances; + } + + Future> tryResolvePath(String path) async { + if (path.startsWith(home)) { + final homeDir_ = homeDir; + assert(homeDir_ != null); + path = path.replaceAll('$home/', homeDir!.path); + } + + final result = []; + final fileSystemEntities = await Glob(path).list().toList(); + for (final fileSystemEntity in fileSystemEntities) { + if (await fileSystemEntity.exists()) { + result.add(fileSystemEntity.uri); + } + } + return result; + } + + static final Uri? homeDir = () { + final path = + Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; + if (path == null) return null; + return Directory(path).uri; + }(); +} + +class RelativeToolResolver implements ToolResolver { + final String toolName; + final ToolResolver wrappedResolver; + final Uri relativePath; + + RelativeToolResolver({ + required this.toolName, + required this.wrappedResolver, + required this.relativePath, + }); + + @override + Future> resolve({Logger? logger}) async { + final otherToolInstances = await wrappedResolver.resolve(logger: logger); + + logger?.finer('Looking for $toolName relative to $otherToolInstances ' + 'with $relativePath.'); + final result = [ + for (final toolInstance in otherToolInstances) + ToolInstance( + tool: Tool(name: toolName), + uri: toolInstance.uri.resolveUri(relativePath), + ), + ]; + if (result.isNotEmpty) { + logger?.fine('Found $result.'); + } else { + logger?.finer('Found no $toolName relative to $otherToolInstances.'); + } + return result; + } +} diff --git a/pkgs/c_compiler/lib/src/utils/run_process.dart b/pkgs/c_compiler/lib/src/utils/run_process.dart new file mode 100644 index 0000000000..10b32e1b01 --- /dev/null +++ b/pkgs/c_compiler/lib/src/utils/run_process.dart @@ -0,0 +1,111 @@ +// 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:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:logging/logging.dart'; + +/// Runs a [Process]. +/// +/// If [logger] is provided, stream stdout and stderr to it. +/// +/// If [captureOutput], captures stdout and stderr. +Future runProcess({ + required Uri executable, + List arguments = const [], + Uri? workingDirectory, + Map? environment, + bool includeParentEnvironment = true, + Logger? logger, + bool captureOutput = true, +}) async { + final printWorkingDir = + workingDirectory != null && workingDirectory != Directory.current.uri; + final commandString = [ + if (printWorkingDir) '(cd ${workingDirectory.path};', + ...?environment?.entries.map((entry) => '${entry.key}=${entry.value}'), + executable, + ...arguments.map((a) => a.contains(' ') ? "'$a'" : a), + if (printWorkingDir) ')', + ].join(' '); + logger?.info('Running `$commandString`.'); + + final stdoutBuffer = StringBuffer(); + final stderrBuffer = StringBuffer(); + final stdoutCompleter = Completer(); + final stderrCompleter = Completer(); + final process = await Process.start( + executable.toFilePath(), + arguments, + workingDirectory: workingDirectory?.toFilePath(), + environment: environment, + includeParentEnvironment: includeParentEnvironment, + ); + + process.stdout.transform(utf8.decoder).listen( + (s) { + logger?.fine(' $s'); + if (captureOutput) stdoutBuffer.write(s); + }, + onDone: stdoutCompleter.complete, + ); + process.stderr.transform(utf8.decoder).listen( + (s) { + logger?.severe(' $s'); + if (captureOutput) stderrBuffer.write(s); + }, + onDone: stderrCompleter.complete, + ); + + final exitCode = await process.exitCode; + await stdoutCompleter.future; + await stderrCompleter.future; + final result = RunProcessResult( + pid: process.pid, + command: commandString, + exitCode: exitCode, + stdout: stdoutBuffer.toString(), + stderr: stderrBuffer.toString(), + ); + return result; +} + +class RunProcessResult extends ProcessResult { + final String command; + + final int _exitCode; + + // For some reason super.exitCode returns 0. + @override + int get exitCode => _exitCode; + + final String _stderrString; + + @override + String get stderr => _stderrString; + + final String _stdoutString; + + @override + String get stdout => _stdoutString; + + RunProcessResult({ + required int pid, + required this.command, + required int exitCode, + required String stderr, + required String stdout, + }) : _exitCode = exitCode, + _stderrString = stderr, + _stdoutString = stdout, + super(pid, exitCode, stdout, stderr); + + @override + String toString() => '''command: $command +exitCode: $exitCode +stdout: $stdout +stderr: $stderr'''; +} diff --git a/pkgs/c_compiler/lib/src/utils/sem_version.dart b/pkgs/c_compiler/lib/src/utils/sem_version.dart new file mode 100644 index 0000000000..1ee493ff85 --- /dev/null +++ b/pkgs/c_compiler/lib/src/utils/sem_version.dart @@ -0,0 +1,18 @@ +// 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 'package:pub_semver/pub_semver.dart'; + +Version? versionFromString(String containsVersion) { + final match = _semverRegex.firstMatch(containsVersion); + if (match == null) { + return null; + } + return Version(int.parse(match.group(1)!), int.parse(match.group(2)!), + int.parse(match.group(3)!), + pre: match.group(4), build: match.group(5)); +} + +final _semverRegex = RegExp( + r'(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?'); diff --git a/pkgs/c_compiler/pubspec.yaml b/pkgs/c_compiler/pubspec.yaml index b224592db8..948eef51a5 100644 --- a/pkgs/c_compiler/pubspec.yaml +++ b/pkgs/c_compiler/pubspec.yaml @@ -1,11 +1,23 @@ name: c_compiler description: A library to invoke the native C compiler installed on the host machine. version: 0.1.0-dev -repository: https://github.com/dart-lang/native/c_compiler +repository: https://github.com/dart-lang/native/tree/main/pkgs/c_compiler environment: sdk: ">=2.19.3 <4.0.0" +publish_to: none + +dependencies: + cli_config: ^0.1.1 + glob: ^2.1.1 + logging: ^1.1.1 + # TODO(dacoharkes): Publish native_assets_cli first. + native_assets_cli: + path: ../native_assets_cli/ + pub_semver: ^2.1.3 + dev_dependencies: + collection: ^1.17.1 dart_flutter_team_lints: ^1.0.0 test: ^1.21.0 diff --git a/pkgs/c_compiler/test/c_compiler_test.dart b/pkgs/c_compiler/test/c_compiler_test.dart deleted file mode 100644 index 65006b3499..0000000000 --- a/pkgs/c_compiler/test/c_compiler_test.dart +++ /dev/null @@ -1,20 +0,0 @@ -// 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 'package:c_compiler/c_compiler.dart'; -import 'package:test/test.dart'; - -void main() { - group('A group of tests', () { - final awesome = Awesome(); - - setUp(() { - // Additional setup goes here. - }); - - test('First Test', () { - expect(awesome.isAwesome, isTrue); - }); - }); -} diff --git a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_android_test.dart b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_android_test.dart new file mode 100644 index 0000000000..bffe5e0636 --- /dev/null +++ b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_android_test.dart @@ -0,0 +1,73 @@ +// 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 'package:c_compiler/c_compiler.dart'; +import 'package:c_compiler/src/utils/run_process.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + const targets = [ + Target.androidArm, + Target.androidArm64, + Target.androidIA32, + Target.androidX64, + ]; + + const readElfMachine = { + Target.androidArm: 'ARM', + Target.androidArm64: 'AArch64', + Target.androidIA32: 'Intel 80386', + Target.androidX64: 'Advanced Micro Devices X86-64', + }; + + for (final packaging in Packaging.values) { + for (final target in targets) { + test('Cbuilder $packaging library $target', () async { + await inTempDir((tempUri) async { + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + target: target, + packaging: packaging == Packaging.dynamic + ? PackagingPreference.dynamic + : PackagingPreference.static, + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: 'add', + assetName: 'add', + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final libUri = + tempUri.resolve(target.os.libraryFileName(name, packaging)); + final result = await runProcess( + executable: Uri.file('readelf'), + arguments: ['-h', libUri.path], + logger: logger, + ); + expect(result.exitCode, 0); + final machine = result.stdout + .split('\n') + .firstWhere((e) => e.contains('Machine:')); + expect(machine, contains(readElfMachine[target])); + expect(result.exitCode, 0); + }); + }); + } + } +} diff --git a/pkgs/c_compiler/test/cbuilder/cbuilder_cross_linux_host_test.dart b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_linux_host_test.dart new file mode 100644 index 0000000000..e52718006e --- /dev/null +++ b/pkgs/c_compiler/test/cbuilder/cbuilder_cross_linux_host_test.dart @@ -0,0 +1,73 @@ +// 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 'package:c_compiler/c_compiler.dart'; +import 'package:c_compiler/src/utils/run_process.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + const targets = [ + Target.linuxArm, + Target.linuxArm64, + Target.linuxIA32, + Target.linuxX64 + ]; + + const readElfMachine = { + Target.linuxArm: 'ARM', + Target.linuxArm64: 'AArch64', + Target.linuxIA32: 'Intel 80386', + Target.linuxX64: 'Advanced Micro Devices X86-64', + }; + + for (final packaging in Packaging.values) { + for (final target in targets) { + test('Cbuilder $packaging library linux $target', () async { + await inTempDir((tempUri) async { + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + target: target, + packaging: packaging == Packaging.dynamic + ? PackagingPreference.dynamic + : PackagingPreference.static, + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + name: 'add', + assetName: 'add', + sources: [addCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final libUri = + tempUri.resolve(target.os.libraryFileName(name, packaging)); + final result = await runProcess( + executable: Uri.file('readelf'), + arguments: ['-h', libUri.path], + logger: logger, + ); + expect(result.exitCode, 0); + final machine = result.stdout + .split('\n') + .firstWhere((e) => e.contains('Machine:')); + expect(machine, contains(readElfMachine[target])); + expect(result.exitCode, 0); + }); + }); + } + } +} diff --git a/pkgs/c_compiler/test/cbuilder/cbuilder_test.dart b/pkgs/c_compiler/test/cbuilder/cbuilder_test.dart new file mode 100644 index 0000000000..c18cea0f16 --- /dev/null +++ b/pkgs/c_compiler/test/cbuilder/cbuilder_test.dart @@ -0,0 +1,85 @@ +// 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:ffi'; +import 'dart:io'; + +import 'package:c_compiler/c_compiler.dart'; +import 'package:c_compiler/src/utils/run_process.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + test('Cbuilder executable', () async { + await inTempDir((tempUri) async { + final helloWorldCUri = packageUri + .resolve('test/cbuilder/testfiles/hello_world/src/hello_world.c'); + if (!await File.fromUri(helloWorldCUri).exists()) { + throw Exception('Run the test from the root directory.'); + } + const name = 'hello_world'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + target: Target.current, + packaging: PackagingPreference.dynamic, // Ignored by executables. + cc: cc, + ); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.executable( + name: name, + sources: [helloWorldCUri.toFilePath()], + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final executableUri = + tempUri.resolve(Target.current.os.executableFileName(name)); + expect(await File.fromUri(executableUri).exists(), true); + final result = await runProcess(executable: executableUri); + expect(result.exitCode, 0); + expect(result.stdout, 'Hello world.\n'); + }); + }); + + test('Cbuilder dylib', () async { + await inTempDir((tempUri) async { + final addCUri = + packageUri.resolve('test/cbuilder/testfiles/add/src/add.c'); + const name = 'add'; + + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + target: Target.current, + packaging: PackagingPreference.dynamic, + cc: cc, + ); + final buildOutput = BuildOutput(); + + final cbuilder = CBuilder.library( + sources: [addCUri.toFilePath()], + name: name, + assetName: name, + ); + await cbuilder.run( + buildConfig: buildConfig, + buildOutput: buildOutput, + logger: logger, + ); + + final dylibUri = tempUri.resolve(Target.current.os.dylibFileName(name)); + final dylib = DynamicLibrary.open(dylibUri.path); + final add = dylib.lookupFunction('add'); + expect(add(1, 2), 3); + }); + }); +} diff --git a/pkgs/c_compiler/test/cbuilder/compiler_resolver_test.dart b/pkgs/c_compiler/test/cbuilder/compiler_resolver_test.dart new file mode 100644 index 0000000000..f1d4ef54f6 --- /dev/null +++ b/pkgs/c_compiler/test/cbuilder/compiler_resolver_test.dart @@ -0,0 +1,56 @@ +// 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 'package:c_compiler/c_compiler.dart'; +import 'package:c_compiler/src/cbuilder/compiler_resolver.dart'; +import 'package:c_compiler/src/tool/tool_error.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + test('Config provided compiler', () async { + await inTempDir((tempUri) async { + final ar = + (await llvmAr.defaultResolver!.resolve(logger: logger)).first.uri; + final cc = + (await clang.defaultResolver!.resolve(logger: logger)).first.uri; + final ld = (await lld.defaultResolver!.resolve(logger: logger)).first.uri; + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + target: Target.current, + packaging: PackagingPreference.dynamic, + ar: ar, + cc: cc, + ld: ld, + ); + final resolver = + CompilerResolver(buildConfig: buildConfig, logger: logger); + final compiler = await resolver.resolveCompiler(); + final archiver = await resolver.resolveArchiver(); + expect(compiler.uri, buildConfig.cc); + expect(archiver.uri, buildConfig.ar); + }); + }); + + test('No compiler found', () async { + await inTempDir((tempUri) async { + final buildConfig = BuildConfig( + outDir: tempUri, + packageRoot: tempUri, + target: Target.windowsX64, + packaging: PackagingPreference.dynamic, + ); + final resolver = CompilerResolver( + buildConfig: buildConfig, + logger: logger, + host: Target.androidArm64, // This is never a host. + ); + expect(resolver.resolveCompiler, throwsA(isA())); + expect(resolver.resolveArchiver, throwsA(isA())); + }); + }); +} diff --git a/pkgs/c_compiler/lib/src/c_compiler_base.dart b/pkgs/c_compiler/test/cbuilder/testfiles/add/src/add.c similarity index 69% rename from pkgs/c_compiler/lib/src/c_compiler_base.dart rename to pkgs/c_compiler/test/cbuilder/testfiles/add/src/add.c index 57ba552dc1..6bfba2fb9b 100644 --- a/pkgs/c_compiler/lib/src/c_compiler_base.dart +++ b/pkgs/c_compiler/test/cbuilder/testfiles/add/src/add.c @@ -2,7 +2,8 @@ // 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. -/// Checks if you are awesome. Spoiler: you are. -class Awesome { - bool get isAwesome => true; +#include + +int32_t add(int32_t a, int32_t b) { + return a+b; } diff --git a/pkgs/native_assets_cli/lib/src/native_assets_cli_base.dart b/pkgs/c_compiler/test/cbuilder/testfiles/hello_world/src/hello_world.c similarity index 69% rename from pkgs/native_assets_cli/lib/src/native_assets_cli_base.dart rename to pkgs/c_compiler/test/cbuilder/testfiles/hello_world/src/hello_world.c index 57ba552dc1..56180a119f 100644 --- a/pkgs/native_assets_cli/lib/src/native_assets_cli_base.dart +++ b/pkgs/c_compiler/test/cbuilder/testfiles/hello_world/src/hello_world.c @@ -2,7 +2,9 @@ // 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. -/// Checks if you are awesome. Spoiler: you are. -class Awesome { - bool get isAwesome => true; +#include + +int main() { + printf("Hello world.\n"); + return 0; } diff --git a/pkgs/c_compiler/test/helpers.dart b/pkgs/c_compiler/test/helpers.dart new file mode 100644 index 0000000000..fc339ddc45 --- /dev/null +++ b/pkgs/c_compiler/test/helpers.dart @@ -0,0 +1,96 @@ +// 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:async'; +import 'dart:io'; + +import 'package:logging/logging.dart'; +import 'package:test/test.dart'; + +const keepTempKey = 'KEEP_TEMPORARY_DIRECTORIES'; + +Future inTempDir( + Future Function(Uri tempUri) fun, { + String? prefix, +}) async { + final tempDir = await Directory.systemTemp.createTemp(prefix); + try { + await fun(tempDir.uri); + } finally { + if (!Platform.environment.containsKey(keepTempKey) || + Platform.environment[keepTempKey]!.isEmpty) { + await tempDir.delete(recursive: true); + } + } +} + +/// Logger that outputs the full trace when a test fails. +final logger = Logger('') + ..level = Level.ALL + ..onRecord.listen((record) { + printOnFailure('${record.level.name}: ${record.time}: ${record.message}'); + }); + +Logger createCapturingLogger(List capturedMessages) => Logger('') + ..level = Level.ALL + ..onRecord.listen((record) { + printOnFailure('${record.level.name}: ${record.time}: ${record.message}'); + capturedMessages.add(record.message); + }); + +/// Test files are run in a variety of ways, find this package root in all. +/// +/// Test files can be run from source from any working directory. The Dart SDK +/// `tools/test.py` runs them from the root of the SDK for example. +/// +/// Test files can be run from dill from the root of package. `package:test` +/// does this. +/// +/// https://github.com/dart-lang/test/issues/110 +Uri findPackageRoot(String packageName) { + final script = Platform.script; + final fileName = script.name; + if (fileName.endsWith('_test.dart')) { + // We're likely running from source. + var directory = script.resolve('.'); + while (true) { + final dirName = directory.name; + if (dirName == packageName) { + return directory; + } + final parent = directory.resolve('..'); + if (parent == directory) break; + directory = parent; + } + } else if (fileName.endsWith('.dill')) { + final cwd = Directory.current.uri; + final dirName = cwd.name; + if (dirName == packageName) { + return cwd; + } + } + throw StateError("Could not find package root for package '$packageName'. " + 'Tried finding the package root via Platform.script ' + "'${Platform.script.toFilePath()}' and Directory.current " + "'${Directory.current.uri.toFilePath()}'."); +} + +Uri packageUri = findPackageRoot('c_compiler'); + +extension on Uri { + String get name => pathSegments.where((e) => e != '').last; +} + +/// Archiver provided by the environment. +final Uri? ar = Platform.environment['AR']?.asFileUri(); + +/// Compiler provided by the environment. +final Uri? cc = Platform.environment['CC']?.asFileUri(); + +/// Linker provided by the environment. +final Uri? ld = Platform.environment['LD']?.asFileUri(); + +extension on String { + Uri asFileUri() => Uri.file(this); +} diff --git a/pkgs/c_compiler/test/native_toolchain/clang_test.dart b/pkgs/c_compiler/test/native_toolchain/clang_test.dart new file mode 100644 index 0000000000..b8373d7918 --- /dev/null +++ b/pkgs/c_compiler/test/native_toolchain/clang_test.dart @@ -0,0 +1,50 @@ +// 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 'package:c_compiler/src/native_toolchain/clang.dart'; +import 'package:c_compiler/src/tool/tool_instance.dart'; +import 'package:c_compiler/src/tool/tool_requirement.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + test('clang smoke test', () async { + final requirement = + ToolRequirement(clang, minimumVersion: Version(14, 0, 0, pre: '0')); + final resolved = await clang.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); + + test('clang versions', () { + final clangInstance = ToolInstance( + tool: clang, + uri: Uri.file('some/path'), + version: Version.parse('14.0.0-1'), + ); + final requirement = + ToolRequirement(clang, minimumVersion: Version(14, 0, 0, pre: '0')); + final satisfied = requirement.satisfy([clangInstance]); + expect(satisfied?.length, 1); + }); + + test('llvm-ar smoke test', () async { + final requirement = ToolRequirement(llvmAr); + final resolved = await llvmAr.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); + + test('ld test', () async { + final requirement = ToolRequirement(lld); + final resolved = await lld.defaultResolver!.resolve(logger: logger); + expect(resolved.isNotEmpty, true); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 1); + }); +} diff --git a/pkgs/c_compiler/test/native_toolchain/gcc_test.dart b/pkgs/c_compiler/test/native_toolchain/gcc_test.dart new file mode 100644 index 0000000000..3604b17505 --- /dev/null +++ b/pkgs/c_compiler/test/native_toolchain/gcc_test.dart @@ -0,0 +1,52 @@ +// 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 'package:c_compiler/src/native_toolchain/gcc.dart'; +import 'package:c_compiler/src/tool/tool.dart'; +import 'package:c_compiler/src/tool/tool_requirement.dart'; +import 'package:c_compiler/src/tool/tool_resolver.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + void testToolSet(String name, List tools) { + test('gcc cross compilation $name smoke test', () async { + final resolver = ToolResolvers([ + for (final tool in tools) tool.defaultResolver!, + ]); + + final resolved = await resolver.resolve(logger: logger); + printOnFailure(resolved.toString()); + expect(resolved.isNotEmpty, true); + + final requirement = RequireAll([ + for (final tool in tools) ToolRequirement(tool), + ]); + + final satisfied = requirement.satisfy(resolved); + printOnFailure(tools.toString()); + printOnFailure(satisfied.toString()); + expect(satisfied?.length, tools.length); + }); + } + + testToolSet('aarch64LinuxGnuGcc', [ + aarch64LinuxGnuGcc, + aarch64LinuxGnuGccAr, + aarch64LinuxGnuLd, + ]); + + testToolSet('armLinuxGnueabihfGcc', [ + armLinuxGnueabihfGcc, + armLinuxGnueabihfGccAr, + armLinuxGnueabihfLd, + ]); + + testToolSet('i686LinuxGnuGcc', [ + i686LinuxGnuGcc, + i686LinuxGnuGccAr, + i686LinuxGnuLd, + ]); +} diff --git a/pkgs/c_compiler/test/native_toolchain/ndk_test.dart b/pkgs/c_compiler/test/native_toolchain/ndk_test.dart new file mode 100644 index 0000000000..26b993b382 --- /dev/null +++ b/pkgs/c_compiler/test/native_toolchain/ndk_test.dart @@ -0,0 +1,23 @@ +// 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 'package:c_compiler/src/native_toolchain/android_ndk.dart'; +import 'package:c_compiler/src/tool/tool_requirement.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + test('NDK smoke test', () async { + final requirement = RequireAll([ + ToolRequirement(androidNdk), + ToolRequirement(androidNdkClang), + ToolRequirement(androidNdkLlvmAr), + ToolRequirement(androidNdkLld), + ]); + final resolved = await androidNdk.defaultResolver!.resolve(logger: logger); + final satisfied = requirement.satisfy(resolved); + expect(satisfied?.length, 4); + }); +} diff --git a/pkgs/c_compiler/test/native_toolchain/recognizer_test.dart b/pkgs/c_compiler/test/native_toolchain/recognizer_test.dart new file mode 100644 index 0000000000..4531880704 --- /dev/null +++ b/pkgs/c_compiler/test/native_toolchain/recognizer_test.dart @@ -0,0 +1,93 @@ +// 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 'package:c_compiler/src/native_toolchain/android_ndk.dart'; +import 'package:c_compiler/src/native_toolchain/clang.dart'; +import 'package:c_compiler/src/native_toolchain/gcc.dart'; +import 'package:c_compiler/src/native_toolchain/recognizer.dart'; +import 'package:c_compiler/src/tool/tool.dart'; +import 'package:c_compiler/src/tool/tool_instance.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + void recognizeCompilerTest(String name, Tool tool) { + test('recognize compiler $name', () async { + final toolInstance = (await tool.defaultResolver!.resolve(logger: logger)) + .where((element) => element.tool == tool) + .first; + final recognizer = CompilerRecognizer(toolInstance.uri); + final toolInstanceAgain = + (await recognizer.resolve(logger: logger)).first; + expect(toolInstanceAgain, toolInstance); + }); + } + + recognizeCompilerTest('aarch64LinuxGnuGcc', aarch64LinuxGnuGcc); + recognizeCompilerTest('androidNdkClang', androidNdkClang); + recognizeCompilerTest('armLinuxGnueabihfGcc', armLinuxGnueabihfGcc); + recognizeCompilerTest('clang', clang); + recognizeCompilerTest('i686LinuxGnuGcc', i686LinuxGnuGcc); + + test('compiler does not exist', () async { + await inTempDir((tempUri) async { + final recognizer = CompilerRecognizer(tempUri.resolve('asdf')); + final result = await recognizer.resolve(logger: logger); + expect(result, []); + }); + }); + + void recognizeLinkerTest(String name, Tool tool) { + test('recognize compiler $name', () async { + final toolInstance = (await tool.defaultResolver!.resolve(logger: logger)) + .where((element) => element.tool == tool) + .first; + final recognizer = LinkerRecognizer(toolInstance.uri); + final toolInstanceAgain = + (await recognizer.resolve(logger: logger)).first; + expect(toolInstanceAgain, toolInstance); + }); + } + + recognizeLinkerTest('aarch64LinuxGnuLd', aarch64LinuxGnuLd); + recognizeLinkerTest('androidNdkLld', androidNdkLld); + recognizeLinkerTest('armLinuxGnueabihfLd', armLinuxGnueabihfLd); + recognizeLinkerTest('i686LinuxGnuLd', i686LinuxGnuLd); + recognizeLinkerTest('lld', lld); + + test('linker does not exist', () async { + await inTempDir((tempUri) async { + final recognizer = LinkerRecognizer(tempUri.resolve('asdf')); + final result = await recognizer.resolve(logger: logger); + expect(result, []); + }); + }); + + void recognizeArchiverTest(String name, Tool tool) { + test('recognize compiler $name', () async { + final toolInstance = (await tool.defaultResolver!.resolve(logger: logger)) + .where((element) => element.tool == tool) + .first; + final recognizer = ArchiverRecognizer(toolInstance.uri); + final toolInstanceAgain = + (await recognizer.resolve(logger: logger)).first; + expect(toolInstanceAgain, toolInstance); + }); + } + + recognizeArchiverTest('aarch64LinuxGnuGccAr', aarch64LinuxGnuGccAr); + recognizeArchiverTest('androidNdkLlvmAr', androidNdkLlvmAr); + recognizeArchiverTest('armLinuxGnueabihfGccAr', armLinuxGnueabihfGccAr); + recognizeArchiverTest('i686LinuxGnuGccAr', i686LinuxGnuGccAr); + recognizeArchiverTest('llvmAr', llvmAr); + + test('archiver does not exist', () async { + await inTempDir((tempUri) async { + final recognizer = ArchiverRecognizer(tempUri.resolve('asdf')); + final result = await recognizer.resolve(logger: logger); + expect(result, []); + }); + }); +} diff --git a/pkgs/c_compiler/test/tool/tool_instance_test.dart b/pkgs/c_compiler/test/tool/tool_instance_test.dart new file mode 100644 index 0000000000..8c38f8c479 --- /dev/null +++ b/pkgs/c_compiler/test/tool/tool_instance_test.dart @@ -0,0 +1,76 @@ +// 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 'package:c_compiler/c_compiler.dart'; +import 'package:collection/collection.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +void main() { + test('equals and hashCode', () { + final barToolInstance = + ToolInstance(tool: Tool(name: 'bar'), uri: Uri.file('path/to/bar')); + final fooToolInstance = + ToolInstance(tool: Tool(name: 'foo'), uri: Uri.file('path/to/foo')); + + expect(barToolInstance, barToolInstance); + expect(barToolInstance != fooToolInstance, true); + + expect(barToolInstance.hashCode, barToolInstance.hashCode); + expect(barToolInstance.hashCode != fooToolInstance.hashCode, true); + + expect( + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(1, 0, 0), + uri: Uri.file('path/to/bar')) != + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(1, 0, 1), + uri: Uri.file('path/to/bar')), + true); + }); + + test('compareTo', () { + final toolInstances = [ + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(2, 0, 0), + uri: Uri.file('path/to/bar'), + ), + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(1, 0, 0), + uri: Uri.file('path/to/bar')), + ToolInstance( + tool: Tool(name: 'bar'), + uri: Uri.file('path/to/bar'), + ), + ToolInstance( + tool: Tool(name: 'bar'), + uri: Uri.file('path/to/some/other/bar'), + ), + ToolInstance( + tool: Tool(name: 'baz'), + uri: Uri.file('path/to/baz'), + ), + ]; + + final toolInstancesSorted = [...toolInstances]..sort(); + expect(DeepCollectionEquality().equals(toolInstancesSorted, toolInstances), + true); + }); + + test('toString', () { + final instance = ToolInstance( + tool: Tool(name: 'bar'), + version: Version(1, 0, 0), + uri: Uri.file('path/to/bar'), + ); + + expect(instance.toString(), contains('bar')); + expect(instance.toString(), contains('1.0.0')); + expect(instance.toString(), contains('path/to/bar')); + }); +} diff --git a/pkgs/c_compiler/test/tool/tool_requirement_test.dart b/pkgs/c_compiler/test/tool/tool_requirement_test.dart new file mode 100644 index 0000000000..87d0a6162e --- /dev/null +++ b/pkgs/c_compiler/test/tool/tool_requirement_test.dart @@ -0,0 +1,48 @@ +// 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 'package:c_compiler/src/tool/tool.dart'; +import 'package:c_compiler/src/tool/tool_instance.dart'; +import 'package:c_compiler/src/tool/tool_requirement.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +void main() { + test('toString', () { + final requirement = + ToolRequirement(Tool(name: 'clang'), minimumVersion: Version(10, 0, 0)); + expect(requirement.toString(), contains('clang')); + expect(requirement.toString(), contains('10.0.0')); + }); + + test('RequireOne', () { + final requirement = RequireOne([ + ToolRequirement(Tool(name: 'bar'), minimumVersion: Version(10, 0, 0)), + ToolRequirement(Tool(name: 'foo'), minimumVersion: Version(10, 0, 0)), + ]); + final toolInstances = [ + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(10, 0, 0), + uri: Uri.file('path/to/bar'), + ), + ToolInstance( + tool: Tool(name: 'foo'), + version: Version(9, 0, 0), + uri: Uri.file('path/to/foo'), + ), + ]; + final result = requirement.satisfy(toolInstances); + expect( + result, + [ + ToolInstance( + tool: Tool(name: 'bar'), + version: Version(10, 0, 0), + uri: Uri.file('path/to/bar'), + ) + ], + ); + }); +} diff --git a/pkgs/c_compiler/test/tool/tool_resolver_test.dart b/pkgs/c_compiler/test/tool/tool_resolver_test.dart new file mode 100644 index 0000000000..c02493c150 --- /dev/null +++ b/pkgs/c_compiler/test/tool/tool_resolver_test.dart @@ -0,0 +1,81 @@ +// 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:io'; + +import 'package:c_compiler/src/native_toolchain/clang.dart'; +import 'package:c_compiler/src/tool/tool.dart'; +import 'package:c_compiler/src/tool/tool_error.dart'; +import 'package:c_compiler/src/tool/tool_instance.dart'; +import 'package:c_compiler/src/tool/tool_resolver.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + test('CliVersionResolver.executableVersion', () async { + final clangInstances = await clang.defaultResolver!.resolve(logger: logger); + expect(clangInstances.isNotEmpty, true); + final version = + await CliVersionResolver.executableVersion(clangInstances.first.uri); + expect(version.major > 5, true); + expect( + () => CliVersionResolver.executableVersion(clangInstances.first.uri, + expectedExitCode: 9999), + throwsA(isA()), + ); + + try { + await CliVersionResolver.executableVersion(clangInstances.first.uri, + expectedExitCode: 9999); + // ignore: avoid_catching_errors + } on ToolError catch (e) { + expect(e.toString(), contains('returned unexpected exit code')); + } + }); + + test('RelativeToolResolver', () async { + await inTempDir((tempUri) async { + final barExeUri = + tempUri.resolve(Target.current.os.executableFileName('bar')); + final bazExeName = Target.current.os.executableFileName('baz'); + final bazExeUri = tempUri.resolve(bazExeName); + await File.fromUri(barExeUri).writeAsString('dummy'); + await File.fromUri(bazExeUri).writeAsString('dummy'); + final barResolver = InstallLocationResolver( + toolName: 'bar', paths: [barExeUri.toFilePath()]); + final bazResolver = RelativeToolResolver( + toolName: 'baz', + wrappedResolver: barResolver, + relativePath: Uri(path: bazExeName), + ); + final resolvedBazInstances = await bazResolver.resolve(logger: logger); + expect( + resolvedBazInstances, + [ToolInstance(tool: Tool(name: 'baz'), uri: bazExeUri)], + ); + }); + }); + + test('logger', () async { + await inTempDir((tempUri) async { + final barExeUri = + tempUri.resolve(Target.current.os.executableFileName('bar')); + final bazExeName = Target.current.os.executableFileName('baz'); + final bazExeUri = tempUri.resolve(bazExeName); + await File.fromUri(barExeUri).writeAsString('dummy'); + final barResolver = InstallLocationResolver( + toolName: 'bar', paths: [barExeUri.toFilePath()]); + final bazResolver = InstallLocationResolver( + toolName: 'baz', paths: [bazExeUri.toFilePath()]); + final barLogs = []; + final bazLogs = []; + await barResolver.resolve(logger: createCapturingLogger(barLogs)); + await bazResolver.resolve(logger: createCapturingLogger(bazLogs)); + expect(barLogs.join('\n'), contains('Found [ToolInstance(bar')); + expect(bazLogs.join('\n'), contains('Found no baz')); + }); + }); +} diff --git a/pkgs/c_compiler/test/tool/tool_test.dart b/pkgs/c_compiler/test/tool/tool_test.dart new file mode 100644 index 0000000000..3fdcf6d261 --- /dev/null +++ b/pkgs/c_compiler/test/tool/tool_test.dart @@ -0,0 +1,27 @@ +// 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 'package:c_compiler/src/native_toolchain/android_ndk.dart'; +import 'package:c_compiler/src/native_toolchain/clang.dart'; +import 'package:c_compiler/src/tool/tool.dart'; +import 'package:c_compiler/src/tool/tool_resolver.dart'; +import 'package:test/test.dart'; + +void main() { + test('equals and hashCode', () async { + expect(clang, clang); + expect(clang != androidNdk, true); + expect( + Tool(name: 'foo'), + Tool(name: 'foo', defaultResolver: PathToolResolver(toolName: 'foo')), + ); + expect(Tool(name: 'foo') != Tool(name: 'bar'), true); + expect( + Tool(name: 'foo').hashCode, + Tool(name: 'foo', defaultResolver: PathToolResolver(toolName: 'foo')) + .hashCode, + ); + expect(Tool(name: 'foo').hashCode != Tool(name: 'bar').hashCode, true); + }); +} diff --git a/pkgs/c_compiler/test/utils/run_process_test.dart b/pkgs/c_compiler/test/utils/run_process_test.dart new file mode 100644 index 0000000000..3968b68195 --- /dev/null +++ b/pkgs/c_compiler/test/utils/run_process_test.dart @@ -0,0 +1,48 @@ +// 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:io'; + +import 'package:c_compiler/src/utils/run_process.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() { + final whichUri = Uri.file(Platform.isWindows ? 'where' : 'which'); + + test('log contains working dir', () async { + await inTempDir((tempUri) async { + final messages = []; + await runProcess( + executable: whichUri, + workingDirectory: tempUri, + logger: createCapturingLogger(messages), + ); + expect(messages.join('\n'), contains('cd')); + }); + }); + + test('log contains env', () async { + final messages = []; + await runProcess( + executable: whichUri, + environment: {'FOO': 'BAR'}, + logger: createCapturingLogger(messages), + ); + expect(messages.join('\n'), contains('FOO=BAR')); + }); + + test('stderr', () async { + final messages = []; + const filePath = 'a/dart/file/which/does/not/exist.dart'; + final result = await runProcess( + executable: Uri.file(Platform.resolvedExecutable), + arguments: [filePath], + logger: createCapturingLogger(messages), + ); + expect(result.stderr, contains(filePath)); + expect(result.toString(), contains(filePath)); + }); +} diff --git a/pkgs/native_assets_cli/README.md b/pkgs/native_assets_cli/README.md index 7ca67f0876..706459f7c7 100644 --- a/pkgs/native_assets_cli/README.md +++ b/pkgs/native_assets_cli/README.md @@ -1,4 +1,4 @@ -[![package:native_assets_cli](https://github.com/dart-lang/native/actions/workflows/native_assets_cli.yml/badge.svg)](https://github.com/dart-lang/native/actions/workflows/native_assets_cli.yml) +[![package:native_assets_cli](https://github.com/dart-lang/native/actions/workflows/native_assets_cli.yaml/badge.svg)](https://github.com/dart-lang/native/actions/workflows/native_assets_cli.yaml) [![pub package](https://img.shields.io/pub/v/native_assets_cli.svg)](https://pub.dev/packages/native_assets_cli) [![Coverage Status](https://coveralls.io/repos/github/dart-lang/native/badge.svg?branch=main)](https://coveralls.io/github/dart-lang/tools?branch=main) diff --git a/pkgs/native_assets_cli/example/native_add/build.dart b/pkgs/native_assets_cli/example/native_add/build.dart new file mode 100644 index 0000000000..0d09253e19 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add/build.dart @@ -0,0 +1,23 @@ +// 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 'package:c_compiler/c_compiler.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; + +const packageName = 'native_add'; + +void main(List args) async { + final buildConfig = await BuildConfig.fromArgs(args); + final buildOutput = BuildOutput(); + final cbuilder = CBuilder.library( + name: packageName, + assetName: + 'package:$packageName/src/${packageName}_bindings_generated.dart', + sources: [ + 'src/$packageName.c', + ], + ); + await cbuilder.run(buildConfig: buildConfig, buildOutput: buildOutput); + await buildOutput.writeToFile(outDir: buildConfig.outDir); +} diff --git a/pkgs/native_assets_cli/example/native_add/ffigen.yaml b/pkgs/native_assets_cli/example/native_add/ffigen.yaml new file mode 100644 index 0000000000..b8716bd2d5 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add/ffigen.yaml @@ -0,0 +1,20 @@ +# Run with `flutter pub run ffigen --config ffigen.yaml`. +name: NativeAddBindings +description: | + Bindings for `src/native_add.h`. + + Regenerate bindings with `flutter pub run ffigen --config ffigen.yaml`. +output: "lib/src/native_add_bindings_generated.dart" +headers: + entry-points: + - "src/native_add.h" + include-directives: + - "src/native_add.h" +preamble: | + // 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. +comments: + style: any + length: full +ffi-native: diff --git a/pkgs/native_assets_cli/example/native_add/lib/native_add.dart b/pkgs/native_assets_cli/example/native_add/lib/native_add.dart new file mode 100644 index 0000000000..9eda681ab4 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add/lib/native_add.dart @@ -0,0 +1,5 @@ +// 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. + +export 'src/native_add.dart'; diff --git a/pkgs/native_assets_cli/example/native_add/lib/src/native_add.dart b/pkgs/native_assets_cli/example/native_add/lib/src/native_add.dart new file mode 100644 index 0000000000..8d05091683 --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add/lib/src/native_add.dart @@ -0,0 +1,9 @@ +// 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 'native_add_bindings_generated.dart' as bindings; + +// TODO(dacoharkes): This will fail lookup until the Dart SDK consumes the +// output of `build.dart`. +int add(int a, int b) => bindings.add(a, b); diff --git a/pkgs/native_assets_cli/example/native_add/lib/src/native_add_bindings_generated.dart b/pkgs/native_assets_cli/example/native_add/lib/src/native_add_bindings_generated.dart new file mode 100644 index 0000000000..2c09b35e0f --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add/lib/src/native_add_bindings_generated.dart @@ -0,0 +1,16 @@ +// 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. + +// AUTO GENERATED FILE, DO NOT EDIT. +// +// Generated by `package:ffigen`. +// ignore_for_file: type=lint +import 'dart:ffi' as ffi; + +// ignore: deprecated_member_use +@ffi.FfiNative('add') +external int add( + int a, + int b, +); diff --git a/pkgs/native_assets_cli/example/native_add/pubspec.yaml b/pkgs/native_assets_cli/example/native_add/pubspec.yaml new file mode 100644 index 0000000000..86acd0dd0a --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add/pubspec.yaml @@ -0,0 +1,22 @@ +publish_to: none + +name: native_add +description: Sums two numbers with native code. +version: 0.1.0 +repository: https://github.com/dcharkes/native_assets_cli + +environment: + sdk: ">=2.19.3 <4.0.0" + +dependencies: + c_compiler: + path: ../../../c_compiler/ + cli_config: ^0.1.1 + logging: ^1.1.1 + native_assets_cli: + path: ../../ + +dev_dependencies: + ffigen: ^7.2.8 + lints: ^2.0.0 + test: ^1.21.0 diff --git a/pkgs/native_assets_cli/example/native_add/src/native_add.c b/pkgs/native_assets_cli/example/native_add/src/native_add.c new file mode 100644 index 0000000000..cf21076d1d --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add/src/native_add.c @@ -0,0 +1,9 @@ +// 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. + +#include "native_add.h" + +int32_t add(int32_t a, int32_t b) { + return a + b; +} diff --git a/pkgs/native_assets_cli/example/native_add/src/native_add.h b/pkgs/native_assets_cli/example/native_add/src/native_add.h new file mode 100644 index 0000000000..5824f98a7d --- /dev/null +++ b/pkgs/native_assets_cli/example/native_add/src/native_add.h @@ -0,0 +1,13 @@ +// 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. + +#include + +#if _WIN32 +#define MYLIB_EXPORT __declspec(dllexport) +#else +#define MYLIB_EXPORT +#endif + +MYLIB_EXPORT int32_t add(int32_t a, int32_t b); diff --git a/pkgs/native_assets_cli/lib/native_assets_cli.dart b/pkgs/native_assets_cli/lib/native_assets_cli.dart index e440c38a74..2986e641ef 100644 --- a/pkgs/native_assets_cli/lib/native_assets_cli.dart +++ b/pkgs/native_assets_cli/lib/native_assets_cli.dart @@ -4,6 +4,14 @@ /// A library that contains the argument and file formats for implementing a /// native assets CLI. -library; +library native_assets_cli; -export 'src/native_assets_cli_base.dart'; +export 'src/model/asset.dart'; +export 'src/model/build_config.dart'; +export 'src/model/build_output.dart'; +export 'src/model/dependencies.dart'; +export 'src/model/ios_sdk.dart'; +export 'src/model/metadata.dart'; +export 'src/model/packaging.dart'; +export 'src/model/packaging_preference.dart'; +export 'src/model/target.dart'; diff --git a/pkgs/native_assets_cli/lib/src/model/asset.dart b/pkgs/native_assets_cli/lib/src/model/asset.dart new file mode 100644 index 0000000000..265e3a785b --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/asset.dart @@ -0,0 +1,323 @@ +// 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 'package:yaml/yaml.dart'; + +import '../utils/uri.dart'; +import '../utils/yaml.dart'; +import 'packaging.dart'; +import 'target.dart'; + +abstract class AssetPath { + factory AssetPath(String pathType, Uri? uri) { + switch (pathType) { + case AssetAbsolutePath._pathTypeValue: + return AssetAbsolutePath(uri!); + case AssetRelativePath._pathTypeValue: + return AssetRelativePath(uri!); + case AssetSystemPath._pathTypeValue: + return AssetSystemPath(uri!); + case AssetInExecutable._pathTypeValue: + return AssetInExecutable(); + case AssetInProcess._pathTypeValue: + return AssetInProcess(); + } + throw FormatException('Unknown pathType: $pathType.'); + } + + factory AssetPath.fromYaml(YamlMap yamlMap) { + final pathType = yamlMap[_pathTypeKey] as String; + final uriString = yamlMap[_uriKey] as String?; + final uri = uriString != null ? Uri(path: uriString) : null; + return AssetPath(pathType, uri); + } + + Map toYaml(); + List toDartConst(); + + static const _pathTypeKey = 'path_type'; + static const _uriKey = 'uri'; + + Future exists(); +} + +/// Asset at absolute path [uri]. +class AssetAbsolutePath implements AssetPath { + final Uri uri; + + AssetAbsolutePath(this.uri); + + static const _pathTypeValue = 'absolute'; + + @override + Map toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + AssetPath._uriKey: uri.toFilePath(), + }; + + @override + List toDartConst() => [_pathTypeValue, uri.toFilePath()]; + + @override + int get hashCode => Object.hash(uri, 133711); + + @override + bool operator ==(Object other) { + if (other is! AssetAbsolutePath) { + return false; + } + return uri == other.uri; + } + + @override + Future exists() => uri.fileSystemEntity.exists(); +} + +/// Asset is avaliable on a relative path. +/// +/// If [Packaging] of an [Asset] is [Packaging.dynamic], +/// `Platform.script.resolve(uri)` will be used to load the asset at runtime. +class AssetRelativePath implements AssetPath { + final Uri uri; + + AssetRelativePath(this.uri); + + static const _pathTypeValue = 'relative'; + + @override + Map toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + AssetPath._uriKey: uri.toFilePath(), + }; + + @override + List toDartConst() => [_pathTypeValue, uri.toFilePath()]; + + @override + int get hashCode => Object.hash(uri, 133717); + + @override + bool operator ==(Object other) { + if (other is! AssetRelativePath) { + return false; + } + return uri == other.uri; + } + + @override + Future exists() => uri.fileSystemEntity.exists(); +} + +/// Asset is avaliable on the system `PATH`. +/// +/// [uri] only contains a file name. +class AssetSystemPath implements AssetPath { + final Uri uri; + + AssetSystemPath(this.uri); + + static const _pathTypeValue = 'system'; + + @override + Map toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + AssetPath._uriKey: uri.toFilePath(), + }; + + @override + List toDartConst() => [_pathTypeValue, uri.toFilePath()]; + + @override + int get hashCode => Object.hash(uri, 133723); + + @override + bool operator ==(Object other) { + if (other is! AssetSystemPath) { + return false; + } + return uri == other.uri; + } + + @override + Future exists() => Future.value(true); +} + +/// Asset is loaded in the process and symbols are available through +/// `DynamicLibrary.process()`. +class AssetInProcess implements AssetPath { + AssetInProcess._(); + + static final AssetInProcess _singleton = AssetInProcess._(); + + factory AssetInProcess() => _singleton; + + static const _pathTypeValue = 'process'; + + @override + Map toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + }; + + @override + List toDartConst() => [_pathTypeValue]; + + @override + Future exists() => Future.value(true); +} + +/// Asset is embedded in executable and symbols are available through +/// `DynamicLibrary.executable()`. +class AssetInExecutable implements AssetPath { + AssetInExecutable._(); + + static final AssetInExecutable _singleton = AssetInExecutable._(); + + factory AssetInExecutable() => _singleton; + + static const _pathTypeValue = 'executable'; + + @override + Map toYaml() => { + AssetPath._pathTypeKey: _pathTypeValue, + }; + + @override + List toDartConst() => [_pathTypeValue]; + + @override + Future exists() => Future.value(true); +} + +class Asset { + final Packaging packaging; + final String name; + final Target target; + final AssetPath path; + + Asset({ + required this.name, + required this.packaging, + required this.target, + required this.path, + }); + + factory Asset.fromYaml(YamlMap yamlMap) => Asset( + name: yamlMap[_nameKey] as String, + path: AssetPath.fromYaml(yamlMap[_pathKey] as YamlMap), + target: Target.fromString(yamlMap[_targetKey] as String), + packaging: Packaging.fromName(yamlMap[_packagingKey] as String), + ); + + static List listFromYamlString(String yaml) { + final yamlObject = loadYaml(yaml); + if (yamlObject == null) { + return []; + } + return [ + for (final yamlElement in yamlObject as YamlList) + Asset.fromYaml(yamlElement as YamlMap), + ]; + } + + static List listFromYamlList(YamlList yamlList) => [ + for (final yamlElement in yamlList) + Asset.fromYaml(yamlElement as YamlMap), + ]; + + Asset copyWith({ + Packaging? packaging, + String? name, + Target? target, + AssetPath? path, + }) => + Asset( + name: name ?? this.name, + packaging: packaging ?? this.packaging, + target: target ?? this.target, + path: path ?? this.path, + ); + + @override + bool operator ==(Object other) { + if (other is! Asset) { + return false; + } + return other.name == name && + other.packaging == packaging && + other.target == target && + other.path == path; + } + + @override + int get hashCode => Object.hash(name, packaging, target, path); + + Map toYaml() => { + _nameKey: name, + _packagingKey: packaging.name, + _pathKey: path.toYaml(), + _targetKey: target.toString(), + }; + + Map> toDartConst() => { + name: path.toDartConst(), + }; + + String toYamlString() => yamlEncode(toYaml()); + + static const _nameKey = 'name'; + static const _packagingKey = 'packaging'; + static const _pathKey = 'path'; + static const _targetKey = 'target'; + + Future exists() => path.exists(); + + @override + String toString() => 'Asset(${toYaml()})'; +} + +extension AssetIterable on Iterable { + List toYaml() => [for (final item in this) item.toYaml()]; + + String toYamlString() => yamlEncode(toYaml()); + + Iterable wherePackaging(Packaging packaging) => + where((e) => e.packaging == packaging); + + Map> get assetsPerTarget { + final result = >{}; + for (final asset in this) { + final assets = result[asset.target] ?? []; + assets.add(asset); + result[asset.target] = assets; + } + return result; + } + + Map>> toDartConst() => { + for (final entry in assetsPerTarget.entries) + entry.key.toString(): + _combineMaps(entry.value.map((e) => e.toDartConst()).toList()) + }; + + Map toNativeAssetsFileEncoding() => { + 'format-version': [1, 0, 0], + 'native-assets': toDartConst(), + }; + + String toNativeAssetsFile() => yamlEncode(toNativeAssetsFileEncoding()); + + Future allExist() async { + final allResults = await Future.wait(map((e) => e.exists())); + final missing = allResults.contains(false); + return !missing; + } +} + +Map _combineMaps(Iterable> maps) { + final result = {}; + for (final map in maps) { + result.addAll(map); + } + return result; +} diff --git a/pkgs/native_assets_cli/lib/src/model/build_config.dart b/pkgs/native_assets_cli/lib/src/model/build_config.dart new file mode 100644 index 0000000000..a4088e5bd8 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/build_config.dart @@ -0,0 +1,244 @@ +// 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 'package:cli_config/cli_config.dart'; +import 'package:collection/collection.dart'; + +import '../utils/map.dart'; +import '../utils/yaml.dart'; +import 'ios_sdk.dart'; +import 'metadata.dart'; +import 'packaging_preference.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; + + /// 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; + + /// Preferred packaging method for library. + PackagingPreference get packaging => _packaging; + late final PackagingPreference _packaging; + + /// 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? get dependencyMetadata => _dependencyMetadata; + late final Map? _dependencyMetadata; + + /// 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, + Uri? ar, + Uri? cc, + Uri? ld, + required PackagingPreference packaging, + Map? dependencyMetadata, + }) { + final nonValidated = BuildConfig._() + .._outDir = outDir + .._packageRoot = packageRoot + .._target = target + .._targetIOSSdk = targetIOSSdk + .._ar = ar + .._cc = cc + .._ld = ld + .._packaging = packaging + .._dependencyMetadata = dependencyMetadata; + final parsedConfigFile = nonValidated.toYaml(); + final config = Config(fileParsed: parsedConfigFile); + return BuildConfig.fromConfig(config); + } + + BuildConfig._(); + + factory BuildConfig.fromConfig(Config config) { + final result = BuildConfig._(); + final configExceptions = []; + 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; + } + + static Future fromArgs(List args) async { + final config = await Config.fromArgs(args: args); + return BuildConfig.fromConfig(config); + } + + static const outDirConfigKey = 'out_dir'; + static const packageRootConfigKey = 'package_root'; + static const arConfigKey = 'ar'; + static const ccConfigKey = 'cc'; + static const ldConfigKey = 'ld'; + static const dependencyMetadataConfigKey = 'dependency_metadata'; + + List _readFieldsFromConfig() { + var targetSet = false; + return [ + (config) => _config = config, + (config) => _outDir = config.path(outDirConfigKey), + (config) => _packageRoot = config.path(packageRootConfigKey), + (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) => _ar = config.optionalPath(arConfigKey, mustExist: true), + (config) => _cc = config.optionalPath(ccConfigKey, mustExist: true), + (config) => _ld = config.optionalPath(ldConfigKey, mustExist: true), + (config) => _packaging = PackagingPreference.fromString( + config.string( + PackagingPreference.configKey, + validValues: PackagingPreference.values.map((e) => '$e'), + ), + ), + (config) => + _dependencyMetadata = _readDependencyMetadataFromConfig(config) + ]; + } + + Map? _readDependencyMetadataFromConfig(Config config) { + final fileValue = + config.valueOf?>(dependencyMetadataConfigKey); + if (fileValue == null) { + return null; + } + final result = {}; + 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 = {}; + 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 toYaml() => { + outDirConfigKey: _outDir.path, + packageRootConfigKey: _packageRoot.path, + Target.configKey: _target.toString(), + if (_targetIOSSdk != null) IOSSdk.configKey: _targetIOSSdk.toString(), + if (_ar != null) arConfigKey: _ar!.path, + if (_cc != null) ccConfigKey: _cc!.path, + if (_ld != null) ldConfigKey: _ld!.path, + PackagingPreference.configKey: _packaging.toString(), + 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._packageRoot != _packageRoot) return false; + if (other._target != _target) return false; + if (other._targetIOSSdk != _targetIOSSdk) return false; + if (other._ar != _ar) return false; + if (other._cc != _cc) return false; + if (other._ld != _ld) return false; + if (other._packaging != _packaging) return false; + if (!DeepCollectionEquality() + .equals(other._dependencyMetadata, _dependencyMetadata)) return false; + return true; + } + + @override + int get hashCode => Object.hash( + _outDir, + _packageRoot, + _target, + _targetIOSSdk, + _ar, + _cc, + _ld, + _packaging, + DeepCollectionEquality().hash(_dependencyMetadata), + ); + + @override + String toString() => 'BuildConfig(${toYaml()})'; +} diff --git a/pkgs/native_assets_cli/lib/src/model/build_output.dart b/pkgs/native_assets_cli/lib/src/model/build_output.dart new file mode 100644 index 0000000000..31558b2a08 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/build_output.dart @@ -0,0 +1,105 @@ +// 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:io'; + +import 'package:collection/collection.dart'; +import 'package:yaml/yaml.dart'; + +import '../utils/datetime.dart'; +import '../utils/file.dart'; +import '../utils/map.dart'; +import '../utils/yaml.dart'; +import 'asset.dart'; +import 'dependencies.dart'; +import 'metadata.dart'; + +class BuildOutput { + /// Time the build this output belongs to started. + /// + /// Rounded down to whole seconds, because [File.lastModified] is rounded + /// to whole seconds and caching logic compares these timestamps. + final DateTime timestamp; + final List assets; + final Dependencies dependencies; + final Metadata metadata; + + BuildOutput({ + DateTime? timestamp, + List? assets, + Dependencies? dependencies, + Metadata? metadata, + }) : timestamp = (timestamp ?? DateTime.now()).roundDownToSeconds(), + assets = assets ?? [], + dependencies = dependencies ?? Dependencies([]), + metadata = metadata ?? Metadata({}); + + static const _assetsKey = 'assets'; + static const _dependenciesKey = 'dependencies'; + static const _metadataKey = 'metadata'; + static const _timestampKey = 'timestamp'; + + factory BuildOutput.fromYamlString(String yaml) { + final yamlObject = loadYaml(yaml) as YamlMap; + return BuildOutput.fromYaml(yamlObject); + } + + factory BuildOutput.fromYaml(YamlMap yamlMap) => BuildOutput( + timestamp: DateTime.parse(yamlMap[_timestampKey] as String), + assets: Asset.listFromYamlList(yamlMap[_assetsKey] as YamlList), + dependencies: + Dependencies.fromYaml(yamlMap[_dependenciesKey] as YamlList?), + metadata: Metadata.fromYaml(yamlMap[_metadataKey] as YamlMap?), + ); + + Map toYaml() => { + _timestampKey: timestamp.toString(), + _assetsKey: assets.toYaml(), + _dependenciesKey: dependencies.toYaml(), + _metadataKey: metadata.toYaml(), + }..sortOnKey(); + + String toYamlString() => yamlEncode(toYaml()); + + static const fileName = 'build_output.yaml'; + + /// Writes the YAML file from [outDir]/[fileName]. + static Future readFromFile({required Uri outDir}) async { + final buildOutputUri = outDir.resolve(fileName); + final buildOutputFile = File.fromUri(buildOutputUri); + if (!await buildOutputFile.exists()) { + return null; + } + return BuildOutput.fromYamlString(await buildOutputFile.readAsString()); + } + + /// Writes the [toYamlString] to [outDir]/[fileName]. + Future writeToFile({required Uri outDir}) async { + final buildOutputUri = outDir.resolve(fileName); + await File.fromUri(buildOutputUri) + .writeAsStringCreateDirectory(toYamlString()); + } + + @override + String toString() => toYamlString(); + + @override + bool operator ==(Object other) { + if (other is! BuildOutput) { + return false; + } + return other.timestamp == timestamp && + ListEquality().equals(other.assets, assets) && + other.dependencies == dependencies && + other.metadata == metadata; + } + + @override + int get hashCode => Object.hash( + timestamp.hashCode, + ListEquality().hash(assets), + dependencies, + metadata, + ); +} diff --git a/pkgs/native_assets_cli/lib/src/model/dependencies.dart b/pkgs/native_assets_cli/lib/src/model/dependencies.dart new file mode 100644 index 0000000000..db372c9577 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/dependencies.dart @@ -0,0 +1,53 @@ +// 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 'package:collection/collection.dart'; +import 'package:yaml/yaml.dart'; + +import '../utils/file.dart'; +import '../utils/uri.dart'; +import '../utils/yaml.dart'; + +class Dependencies { + final List dependencies; + + const Dependencies(this.dependencies); + + factory Dependencies.fromYamlString(String yamlString) { + final yaml = loadYaml(yamlString); + if (yaml is YamlList) { + return Dependencies.fromYaml(yaml); + } + return Dependencies([]); + } + + factory Dependencies.fromYaml(YamlList? yamlList) => Dependencies([ + if (yamlList != null) + for (final dependency in yamlList) + fileSystemPathToUri(dependency as String), + ]); + + List toYaml() => [ + for (final dependency in dependencies) dependency.path, + ]; + + String toYamlString() => yamlEncode(toYaml()); + + @override + String toString() => toYamlString(); + + Future lastModified() => + dependencies.map((u) => u.fileSystemEntity).lastModified(); + + @override + bool operator ==(Object other) { + if (other is! Dependencies) { + return false; + } + return ListEquality().equals(other.dependencies, dependencies); + } + + @override + int get hashCode => ListEquality().hash(dependencies); +} diff --git a/pkgs/native_assets_cli/lib/src/model/ios_sdk.dart b/pkgs/native_assets_cli/lib/src/model/ios_sdk.dart new file mode 100644 index 0000000000..7d3deeb01d --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/ios_sdk.dart @@ -0,0 +1,29 @@ +// 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. + +/// For an iOS target, a build is either done for the device or the simulator. +/// +/// Only fat binaries or xcframeworks can contain both targets. +class IOSSdk { + final String xcodebuildSdk; + + const IOSSdk._(this.xcodebuildSdk); + + static const iPhoneOs = IOSSdk._('iphoneos'); + static const iPhoneSimulator = IOSSdk._('iphonesimulator'); + + static const values = [ + iPhoneOs, + iPhoneSimulator, + ]; + + factory IOSSdk.fromString(String target) => + values.firstWhere((e) => e.xcodebuildSdk == target); + + /// The `package:config` key preferably used. + static const String configKey = 'target_ios_sdk'; + + @override + String toString() => xcodebuildSdk; +} diff --git a/pkgs/native_assets_cli/lib/src/model/metadata.dart b/pkgs/native_assets_cli/lib/src/model/metadata.dart new file mode 100644 index 0000000000..cf828d20aa --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/metadata.dart @@ -0,0 +1,41 @@ +// 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 'package:collection/collection.dart'; +import 'package:yaml/yaml.dart'; + +import '../utils/map.dart'; +import '../utils/yaml.dart'; + +class Metadata { + final Map metadata; + + const Metadata(this.metadata); + + factory Metadata.fromYaml(YamlMap? yamlMap) => + Metadata(yamlMap?.cast() ?? {}); + + factory Metadata.fromYamlString(String yaml) { + final yamlObject = loadYaml(yaml) as YamlMap; + return Metadata.fromYaml(yamlObject); + } + + Map toYaml() => metadata..sortOnKey(); + + String toYamlString() => yamlEncode(toYaml()); + + @override + bool operator ==(Object other) { + if (other is! Metadata) { + return false; + } + return DeepCollectionEquality().equals(other.metadata, metadata); + } + + @override + int get hashCode => DeepCollectionEquality().hash(metadata); + + @override + String toString() => 'Metadata(${toYaml()})'; +} diff --git a/pkgs/native_assets_cli/lib/src/model/packaging.dart b/pkgs/native_assets_cli/lib/src/model/packaging.dart new file mode 100644 index 0000000000..5cc734a4ae --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/packaging.dart @@ -0,0 +1,24 @@ +// 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. + +class Packaging { + final String name; + + const Packaging._(this.name); + + static const Packaging dynamic = Packaging._('dynamic'); + static const Packaging static = Packaging._('static'); + + /// Known values for [Packaging]. + static const List values = [ + dynamic, + static, + ]; + + factory Packaging.fromName(String name) => + values.where((element) => element.name == name).first; + + @override + String toString() => name; +} diff --git a/pkgs/native_assets_cli/lib/src/model/packaging_preference.dart b/pkgs/native_assets_cli/lib/src/model/packaging_preference.dart new file mode 100644 index 0000000000..d4660ba2e0 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/packaging_preference.dart @@ -0,0 +1,77 @@ +// 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 'packaging.dart'; + +class PackagingPreference { + final String name; + final String description; + final List preferredPackaging; + final List potentialPackaging; + + const PackagingPreference( + this.name, + this.description, { + required this.preferredPackaging, + required this.potentialPackaging, + }); + + factory PackagingPreference.fromString(String name) => + values.where((element) => element.name == name).first; + + static const dynamic = PackagingPreference( + 'dynamic', + '''Provide native assets as dynamic libraries. +Fails if not all native assets can only be provided as static library. +Required to run Dart in JIT mode.''', + preferredPackaging: [Packaging.dynamic], + potentialPackaging: [Packaging.dynamic], + ); + static const static = PackagingPreference( + 'static', + '''Provide native assets as static libraries. +Fails if not all native assets can only be provided as dynamic library. +Required for potential link-time tree-shaking of native code. +Therefore, preferred to in Dart AOT mode.''', + preferredPackaging: [Packaging.static], + potentialPackaging: [Packaging.static], + ); + static const preferDynamic = PackagingPreference( + 'prefer-dynamic', + '''Provide native assets as dynamic libraries, if possible. +Otherwise, build native assets as static libraries.''', + preferredPackaging: [Packaging.dynamic], + potentialPackaging: Packaging.values, + ); + static const preferStatic = PackagingPreference( + 'prefer-static', + '''Provide native assets as static libraries, if possible. +Otherwise, build native assets as dynamic libraries. +Preferred for AOT compilation, if there are any native assets which can only be +provided as dynamic libraries.''', + preferredPackaging: [Packaging.static], + potentialPackaging: Packaging.values, + ); + static const all = PackagingPreference( + 'all', + '''Provide native assets as both dynamic and static libraries if supported. +Mostly useful for testing the build scripts.''', + preferredPackaging: Packaging.values, + potentialPackaging: Packaging.values, + ); + + static const values = [ + dynamic, + static, + preferDynamic, + preferStatic, + all, + ]; + + /// The `package:config` key preferably used. + static const String configKey = 'packaging'; + + @override + String toString() => name; +} diff --git a/pkgs/native_assets_cli/lib/src/model/target.dart b/pkgs/native_assets_cli/lib/src/model/target.dart new file mode 100644 index 0000000000..c9d0c204b4 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/model/target.dart @@ -0,0 +1,326 @@ +// 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:ffi' show Abi; +import 'dart:io'; + +import 'packaging.dart'; + +/// The hardware architectures the Dart VM runs on. +class Architecture { + /// This architecture as used in [Platform.version]. + final String dartPlatform; + + const Architecture._(this.dartPlatform); + + factory Architecture.fromAbi(Abi abi) => _abiToArch[abi]!; + + static const Architecture arm = Architecture._('arm'); + static const Architecture arm64 = Architecture._('arm64'); + static const Architecture ia32 = Architecture._('ia32'); + static const Architecture riscv32 = Architecture._('riscv32'); + static const Architecture riscv64 = Architecture._('riscv64'); + static const Architecture x64 = Architecture._('x64'); + + /// Known values for [Architecture]. + static const List values = [ + arm, + arm64, + ia32, + riscv32, + riscv64, + x64, + ]; + + static const _abiToArch = { + Abi.androidArm: Architecture.arm, + Abi.androidArm64: Architecture.arm64, + Abi.androidIA32: Architecture.ia32, + Abi.androidX64: Architecture.x64, + Abi.fuchsiaArm64: Architecture.arm64, + Abi.fuchsiaX64: Architecture.x64, + Abi.iosArm: Architecture.arm, + Abi.iosArm64: Architecture.arm64, + Abi.iosX64: Architecture.x64, + Abi.linuxArm: Architecture.arm, + Abi.linuxArm64: Architecture.arm64, + Abi.linuxIA32: Architecture.ia32, + Abi.linuxRiscv32: Architecture.riscv32, + Abi.linuxRiscv64: Architecture.riscv64, + Abi.linuxX64: Architecture.x64, + Abi.macosArm64: Architecture.arm64, + Abi.macosX64: Architecture.x64, + Abi.windowsArm64: Architecture.arm64, + Abi.windowsIA32: Architecture.ia32, + Abi.windowsX64: Architecture.x64, + }; +} + +/// The operating systems the Dart VM runs on. +class OS { + /// This OS as used in [Platform.version] + final String dartPlatform; + + const OS._(this.dartPlatform); + + factory OS.fromAbi(Abi abi) => _abiToOS[abi]!; + + static const OS android = OS._('android'); + static const OS fuchsia = OS._('fuchsia'); + static const OS iOS = OS._('ios'); + static const OS linux = OS._('linux'); + static const OS macOS = OS._('macos'); + static const OS windows = OS._('windows'); + + /// Known values for [OS]. + static const List values = [ + android, + fuchsia, + iOS, + linux, + macOS, + windows, + ]; + + static const _abiToOS = { + Abi.androidArm: OS.android, + Abi.androidArm64: OS.android, + Abi.androidIA32: OS.android, + Abi.androidX64: OS.android, + Abi.fuchsiaArm64: OS.fuchsia, + Abi.fuchsiaX64: OS.fuchsia, + Abi.iosArm: OS.iOS, + Abi.iosArm64: OS.iOS, + Abi.iosX64: OS.iOS, + Abi.linuxArm: OS.linux, + Abi.linuxArm64: OS.linux, + Abi.linuxIA32: OS.linux, + Abi.linuxRiscv32: OS.linux, + Abi.linuxRiscv64: OS.linux, + Abi.linuxX64: OS.linux, + Abi.macosArm64: OS.macOS, + Abi.macosX64: OS.macOS, + Abi.windowsArm64: OS.windows, + Abi.windowsIA32: OS.windows, + Abi.windowsX64: OS.windows, + }; + + /// Whether the [OS] is a OS for mobile devices. + bool get isMobile => this == OS.android || this == OS.iOS; + + /// Whether the [OS] is a OS for desktop devices. + bool get isDesktop => + this == OS.linux || this == OS.macOS || this == OS.windows; + + /// Typical cross compilation between OSes. + static const _osCrossCompilationDefault = { + OS.macOS: [OS.macOS, OS.iOS, OS.android], + OS.linux: [OS.linux, OS.android], + OS.windows: [OS.windows, OS.android], + }; + + /// The default dynamic library file name on this [OS]. + String dylibFileName(String name) { + final prefix = _dylibPrefix[this]!; + final extension = _dylibExtension[this]!; + return '$prefix$name.$extension'; + } + + /// The default static library file name on this [OS]. + String staticlibFileName(String name) { + final prefix = _staticlibPrefix[this]!; + final extension = _staticlibExtension[this]!; + return '$prefix$name.$extension'; + } + + String libraryFileName(String name, Packaging packaging) { + if (packaging == Packaging.dynamic) { + return dylibFileName(name); + } + assert(packaging == Packaging.static); + return staticlibFileName(name); + } + + /// The default executable file name on this [OS]. + String executableFileName(String name) { + final extension = _executableExtension[this]!; + final dot = extension.isNotEmpty ? '.' : ''; + return '$name$dot$extension'; + } + + /// The default name prefix for dynamic libraries per [OS]. + static const _dylibPrefix = { + OS.android: 'lib', + OS.fuchsia: 'lib', + OS.iOS: 'lib', + OS.linux: 'lib', + OS.macOS: 'lib', + OS.windows: '', + }; + + /// The default extension for dynamic libraries per [OS]. + static const _dylibExtension = { + OS.android: 'so', + OS.fuchsia: 'so', + OS.iOS: 'dylib', + OS.linux: 'so', + OS.macOS: 'dylib', + OS.windows: 'dll', + }; + + /// The default name prefix for static libraries per [OS]. + static const _staticlibPrefix = _dylibPrefix; + + /// The default extension for static libraries per [OS]. + static const _staticlibExtension = { + OS.android: 'a', + OS.fuchsia: 'a', + OS.iOS: 'a', + OS.linux: 'a', + OS.macOS: 'a', + OS.windows: 'lib', + }; + + /// The default extension for executables per [OS]. + static const _executableExtension = { + OS.android: '', + OS.fuchsia: '', + OS.iOS: '', + OS.linux: '', + OS.macOS: '', + OS.windows: 'exe', + }; +} + +/// Application binary interface. +/// +/// The Dart VM can run on a variety of [Target]s, see [Target.values]. +class Target implements Comparable { + final Abi abi; + + const Target._(this.abi); + + factory Target.fromString(String target) => _stringToTarget[target]!; + + /// The [Target] corresponding the substring of [Platform.version] + /// describing the [Target]. + /// + /// The [Platform.version] strings are formatted as follows: + /// ` () on ""`. + factory Target.fromDartPlatform(String versionStringFull) { + final split = versionStringFull.split('"'); + if (split.length < 2) { + throw FormatException( + "Unknown version from Platform.version '$versionStringFull'."); + } + final versionString = split[1]; + final target = _dartVMstringToTarget[versionString]; + if (target == null) { + throw FormatException("Unknown ABI '$versionString' from Platform.version" + " '$versionStringFull'."); + } + return target; + } + + static const androidArm = Target._(Abi.androidArm); + static const androidArm64 = Target._(Abi.androidArm64); + static const androidIA32 = Target._(Abi.androidIA32); + static const androidX64 = Target._(Abi.androidX64); + static const fuchsiaArm64 = Target._(Abi.fuchsiaArm64); + static const fuchsiaX64 = Target._(Abi.fuchsiaX64); + static const iOSArm = Target._(Abi.iosArm); + static const iOSArm64 = Target._(Abi.iosArm64); + static const linuxArm = Target._(Abi.linuxArm); + static const linuxArm64 = Target._(Abi.linuxArm64); + static const linuxIA32 = Target._(Abi.linuxIA32); + static const linuxRiscv32 = Target._(Abi.linuxRiscv32); + static const linuxRiscv64 = Target._(Abi.linuxRiscv64); + static const linuxX64 = Target._(Abi.linuxX64); + static const macOSArm64 = Target._(Abi.macosArm64); + static const macOSX64 = Target._(Abi.macosX64); + static const windowsIA32 = Target._(Abi.windowsIA32); + static const windowsX64 = Target._(Abi.windowsX64); + + /// All Targets that we can build for. + /// + /// Note that for some of these a Dart SDK is not available and they are only + /// used as target architectures for Flutter apps. + static const values = { + androidArm, + androidArm64, + androidIA32, + androidX64, + fuchsiaArm64, + fuchsiaX64, + iOSArm, + iOSArm64, + linuxArm, + linuxArm64, + linuxIA32, + linuxRiscv32, + linuxRiscv64, + linuxX64, + macOSArm64, + macOSX64, + windowsIA32, + windowsX64, + // TODO(dacoharkes): Add support for `wasm`. + }; + + /// Mapping from strings as used in [Target.toString] to [Target]s. + static final Map _stringToTarget = Map.fromEntries( + Target.values.map((target) => MapEntry(target.toString(), target))); + + /// Mapping from lowercased strings as used in [Platform.version] to + /// [Target]s. + static final Map _dartVMstringToTarget = Map.fromEntries( + Target.values.map((target) => MapEntry(target.dartVMToString(), target))); + + /// The current [Target]. + /// + /// Read from the [Platform.version] string. + static final Target current = Target.fromDartPlatform(Platform.version); + + Architecture get architecture => Architecture.fromAbi(abi); + + OS get os => OS.fromAbi(abi); + + String get _architectureString => architecture.dartPlatform; + + String get _osString => os.dartPlatform; + + /// A string representation of this object. + @override + String toString() => dartVMToString(); + + /// As used in [Platform.version]. + String dartVMToString() => '${_osString}_$_architectureString'; + + /// Compares `this` to [other]. + /// + /// If [other] is also an [Target], consistent with sorting on [toString]. + @override + int compareTo(Target other) => toString().compareTo(other.toString()); + + /// A list of supported target [Target]s from this host [os]. + List supportedTargetTargets( + {Map> osCrossCompilation = + OS._osCrossCompilationDefault}) => + Target.values + .where((target) => + // Only valid cross compilation. + osCrossCompilation[os]!.contains(target.os) && + // And no deprecated architectures. + target != Target.iOSArm) + .sorted; + + /// The `package:config` key preferably used. + static const String configKey = 'target'; +} + +/// Common methods for manipulating iterables of [Target]s. +extension TargetList on Iterable { + /// The [Target]s in `this` sorted by name alphabetically. + List get sorted => [for (final target in this) target]..sort(); +} diff --git a/pkgs/native_assets_cli/lib/src/utils/datetime.dart b/pkgs/native_assets_cli/lib/src/utils/datetime.dart new file mode 100644 index 0000000000..24842c131a --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/datetime.dart @@ -0,0 +1,9 @@ +// 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. + +extension DateTimeExtension on DateTime { + DateTime roundDownToSeconds() => + DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch - + millisecondsSinceEpoch % Duration(seconds: 1).inMilliseconds); +} diff --git a/pkgs/native_assets_cli/lib/src/utils/file.dart b/pkgs/native_assets_cli/lib/src/utils/file.dart new file mode 100644 index 0000000000..703f601e50 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/file.dart @@ -0,0 +1,65 @@ +// 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'; + +extension FileExtension on File { + Future writeAsStringCreateDirectory(String contents, + {FileMode mode = FileMode.write, + Encoding encoding = utf8, + bool flush = false}) async { + if (!await parent.exists()) { + await parent.create(recursive: true); + } + return await writeAsString(contents, + mode: mode, encoding: encoding, flush: flush); + } +} + +extension FileSystemEntityExtension on FileSystemEntity { + Future lastModified() async { + final this_ = this; + if (this_ is Link || await FileSystemEntity.isLink(this_.path)) { + // Don't follow links. + return DateTime.fromMicrosecondsSinceEpoch(0); + } + if (this_ is File) { + if (!await this_.exists()) { + // If the file was deleted, regard it is modified recently. + return DateTime.now(); + } + return await this_.lastModified(); + } + assert(this_ is Directory); + this_ as Directory; + return await this_.lastModified(); + } +} + +extension FileSystemEntityIterable on Iterable { + Future lastModified() async { + var last = DateTime.fromMillisecondsSinceEpoch(0); + for (final entity in this) { + final entityTimestamp = await entity.lastModified(); + if (entityTimestamp.isAfter(last)) { + last = entityTimestamp; + } + } + return last; + } +} + +extension DirectoryExtension on Directory { + Future lastModified() async { + var last = DateTime.fromMillisecondsSinceEpoch(0); + await for (final entity in list()) { + final entityTimestamp = await entity.lastModified(); + if (entityTimestamp.isAfter(last)) { + last = entityTimestamp; + } + } + return last; + } +} diff --git a/pkgs/native_assets_cli/lib/src/utils/map.dart b/pkgs/native_assets_cli/lib/src/utils/map.dart new file mode 100644 index 0000000000..bb767ed991 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/map.dart @@ -0,0 +1,15 @@ +// 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. + +extension MapSorting, V extends Object> on Map { + Map sortOnKey() { + final result = {}; + final keysSorted = keys.toList()..sort(); + for (final key in keysSorted) { + final value = this[key]!; + result[key] = value; + } + return result; + } +} diff --git a/pkgs/native_assets_cli/lib/src/utils/uri.dart b/pkgs/native_assets_cli/lib/src/utils/uri.dart new file mode 100644 index 0000000000..ee4fd032a7 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/uri.dart @@ -0,0 +1,21 @@ +// 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:io'; + +extension UriExtension on Uri { + FileSystemEntity get fileSystemEntity { + if (path.endsWith(Platform.pathSeparator) || path.endsWith('/')) { + return Directory.fromUri(this); + } + return File.fromUri(this); + } +} + +Uri fileSystemPathToUri(String path) { + if (path.endsWith(Platform.pathSeparator) || path.endsWith('/')) { + return Uri.directory(path); + } + return Uri.file(path); +} diff --git a/pkgs/native_assets_cli/lib/src/utils/yaml.dart b/pkgs/native_assets_cli/lib/src/utils/yaml.dart new file mode 100644 index 0000000000..b62746bb12 --- /dev/null +++ b/pkgs/native_assets_cli/lib/src/utils/yaml.dart @@ -0,0 +1,18 @@ +// 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 'package:yaml/yaml.dart'; +import 'package:yaml_edit/yaml_edit.dart'; + +String yamlEncode(Object yamlEncoding) { + final editor = YamlEditor(''); + editor.update( + [], + wrapAsYamlNode( + yamlEncoding, + collectionStyle: CollectionStyle.BLOCK, + ), + ); + return editor.toString(); +} diff --git a/pkgs/native_assets_cli/pubspec.yaml b/pkgs/native_assets_cli/pubspec.yaml index 5039b5984f..a1b6902c48 100644 --- a/pkgs/native_assets_cli/pubspec.yaml +++ b/pkgs/native_assets_cli/pubspec.yaml @@ -1,11 +1,17 @@ name: native_assets_cli description: A library that contains the argument and file formats for implementing a native assets CLI. version: 0.1.0-dev -repository: https://github.com/dart-lang/native/native_assets_cli +repository: https://github.com/dart-lang/native/tree/main/pkgs/native_assets_cli environment: sdk: ">=2.19.3 <4.0.0" +dependencies: + cli_config: ^0.1.1 + collection: ^1.17.1 + yaml: ^3.1.1 + yaml_edit: ^2.1.0 + dev_dependencies: dart_flutter_team_lints: ^1.0.0 test: ^1.21.0 diff --git a/pkgs/native_assets_cli/test/example/native_add_test.dart b/pkgs/native_assets_cli/test/example/native_add_test.dart new file mode 100644 index 0000000000..b6c771ee9d --- /dev/null +++ b/pkgs/native_assets_cli/test/example/native_add_test.dart @@ -0,0 +1,63 @@ +// 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:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; + +void main() async { + late Uri tempUri; + + setUp(() async { + tempUri = (await Directory.systemTemp.createTemp()).uri; + }); + + tearDown(() async { + await Directory.fromUri(tempUri).delete(recursive: true); + }); + + test('native_add build', () async { + final testTempUri = tempUri.resolve('test1/'); + await Directory.fromUri(testTempUri).create(); + final testPackageUri = packageUri.resolve('example/native_add/'); + final dartUri = Uri.file(Platform.resolvedExecutable); + + final processResult = await Process.run( + dartUri.path, + [ + 'build.dart', + '-Dout_dir=${tempUri.path}', + '-Dpackage_root=${testPackageUri.path}', + '-Dtarget=${Target.current}', + '-Dpackaging=dynamic', + if (cc != null) '-Dcc=${cc!.toFilePath()}', + ], + workingDirectory: testPackageUri.path, + ); + if (processResult.exitCode != 0) { + print(processResult.stdout); + print(processResult.stderr); + print(processResult.exitCode); + } + expect(processResult.exitCode, 0); + + final buildOutputUri = tempUri.resolve('build_output.yaml'); + final buildOutput = BuildOutput.fromYamlString( + await File.fromUri(buildOutputUri).readAsString()); + final assets = buildOutput.assets; + expect(assets.length, 1); + final dependencies = buildOutput.dependencies; + expect(await assets.allExist(), true); + expect( + dependencies.dependencies, + [ + testPackageUri.resolve('src/native_add.c'), + testPackageUri.resolve('build.dart'), + ], + ); + }); +} diff --git a/pkgs/native_assets_cli/test/helpers.dart b/pkgs/native_assets_cli/test/helpers.dart new file mode 100644 index 0000000000..5e2b000627 --- /dev/null +++ b/pkgs/native_assets_cli/test/helpers.dart @@ -0,0 +1,61 @@ +// 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:io'; + +/// Test files are run in a variety of ways, find this package root in all. +/// +/// Test files can be run from source from any working directory. The Dart SDK +/// `tools/test.py` runs them from the root of the SDK for example. +/// +/// Test files can be run from dill from the root of package. `package:test` +/// does this. +/// +/// https://github.com/dart-lang/test/issues/110 +Uri findPackageRoot(String packageName) { + final script = Platform.script; + final fileName = script.name; + if (fileName.endsWith('_test.dart')) { + // We're likely running from source. + var directory = script.resolve('.'); + while (true) { + final dirName = directory.name; + if (dirName == packageName) { + return directory; + } + final parent = directory.resolve('..'); + if (parent == directory) break; + directory = parent; + } + } else if (fileName.endsWith('.dill')) { + final cwd = Directory.current.uri; + final dirName = cwd.name; + if (dirName == packageName) { + return cwd; + } + } + throw StateError("Could not find package root for package '$packageName'. " + 'Tried finding the package root via Platform.script ' + "'${Platform.script.toFilePath()}' and Directory.current " + "'${Directory.current.uri.toFilePath()}'."); +} + +Uri packageUri = findPackageRoot('native_assets_cli'); + +extension on Uri { + String get name => pathSegments.where((e) => e != '').last; +} + +/// Archiver provided by the environment. +final Uri? ar = Platform.environment['AR']?.asFileUri(); + +/// Compiler provided by the environment. +final Uri? cc = Platform.environment['CC']?.asFileUri(); + +/// Linker provided by the environment. +final Uri? ld = Platform.environment['LD']?.asFileUri(); + +extension on String { + Uri asFileUri() => Uri.file(this); +} diff --git a/pkgs/native_assets_cli/test/model/asset_test.dart b/pkgs/native_assets_cli/test/model/asset_test.dart new file mode 100644 index 0000000000..3a12620185 --- /dev/null +++ b/pkgs/native_assets_cli/test/model/asset_test.dart @@ -0,0 +1,186 @@ +// 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 'package:collection/collection.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + final assets = [ + Asset( + name: 'foo', + path: AssetAbsolutePath(Uri(path: 'path/to/libfoo.so')), + target: Target.androidX64, + packaging: Packaging.dynamic, + ), + Asset( + name: 'foo2', + path: AssetRelativePath(Uri(path: 'path/to/libfoo2.so')), + target: Target.androidX64, + packaging: Packaging.dynamic, + ), + Asset( + name: 'foo3', + path: AssetSystemPath(Uri(path: 'libfoo3.so')), + target: Target.androidX64, + packaging: Packaging.dynamic, + ), + Asset( + name: 'foo4', + path: AssetInExecutable(), + target: Target.androidX64, + packaging: Packaging.dynamic, + ), + Asset( + name: 'foo5', + path: AssetInProcess(), + target: Target.androidX64, + packaging: Packaging.dynamic, + ), + Asset( + name: 'bar', + path: AssetAbsolutePath(Uri(path: 'path/to/libbar.a')), + target: Target.linuxArm64, + packaging: Packaging.static, + ), + Asset( + name: 'bla', + path: AssetAbsolutePath(Uri(path: 'path/with spaces/bla.dll')), + target: Target.windowsX64, + packaging: Packaging.dynamic, + ), + ]; + + const assetsYamlEncoding = '''- name: foo + packaging: dynamic + path: + path_type: absolute + uri: path/to/libfoo.so + target: android_x64 +- name: foo2 + packaging: dynamic + path: + path_type: relative + uri: path/to/libfoo2.so + target: android_x64 +- name: foo3 + packaging: dynamic + path: + path_type: system + uri: libfoo3.so + target: android_x64 +- name: foo4 + packaging: dynamic + path: + path_type: executable + target: android_x64 +- name: foo5 + packaging: dynamic + path: + path_type: process + target: android_x64 +- name: bar + packaging: static + path: + path_type: absolute + uri: path/to/libbar.a + target: linux_arm64 +- name: bla + packaging: dynamic + path: + path_type: absolute + uri: path/with spaces/bla.dll + target: windows_x64'''; + + const assetsDartEncoding = '''format-version: + - 1 + - 0 + - 0 +native-assets: + android_x64: + foo: + - absolute + - path/to/libfoo.so + foo2: + - relative + - path/to/libfoo2.so + foo3: + - system + - libfoo3.so + foo4: + - executable + foo5: + - process + linux_arm64: + bar: + - absolute + - path/to/libbar.a + windows_x64: + bla: + - absolute + - path/with spaces/bla.dll'''; + + test('asset yaml', () { + final yaml = assets.toYamlString().replaceAll('\\', '/'); + expect(yaml, assetsYamlEncoding); + final assets2 = Asset.listFromYamlString(yaml); + expect(assets, assets2); + }); + + test('asset yaml', () async { + final fileContents = assets.toNativeAssetsFile(); + expect(fileContents.replaceAll('\\', '/'), assetsDartEncoding); + }); + + test('AssetPath factory', () async { + expect(() => AssetPath('wrong', null), throwsFormatException); + }); + + test('Asset hashCode copyWith', () async { + final asset = assets.first; + final asset2 = asset.copyWith(name: 'foo321'); + expect(asset.hashCode != asset2.hashCode, true); + + final asset3 = asset.copyWith(); + expect(asset.hashCode, asset3.hashCode); + }); + + test('List hashCode', () async { + final assets2 = assets.take(3).toList(); + final equality = ListEquality(); + expect(equality.hash(assets) != equality.hash(assets2), true); + }); + + test('List wherePackaging', () async { + final assets2 = assets.wherePackaging(Packaging.dynamic); + expect(assets2.length, 6); + }); + + test('Asset toString', () async { + assets.toString(); + }); + + test('Asset toString', () async { + expect(await assets.allExist(), false); + }); + + test('Asset toYaml', () async { + expect( + assets.first.toYamlString(), + ''' +name: foo +packaging: dynamic +path: + path_type: absolute + uri: path/to/libfoo.so +target: android_x64 +''' + .trim()); + }); + + test('Asset listFromYamlString', () async { + final assets = Asset.listFromYamlString(''); + expect(assets, []); + }); +} diff --git a/pkgs/native_assets_cli/test/model/build_config_test.dart b/pkgs/native_assets_cli/test/model/build_config_test.dart new file mode 100644 index 0000000000..d9ab529e27 --- /dev/null +++ b/pkgs/native_assets_cli/test/model/build_config_test.dart @@ -0,0 +1,276 @@ +// 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:io'; + +import 'package:cli_config/cli_config.dart'; +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() async { + late Uri tempUri; + late Uri fakeClang; + late Uri fakeLd; + late Uri fakeAr; + + setUp(() async { + tempUri = (await Directory.systemTemp.createTemp()).uri; + fakeClang = tempUri.resolve('fake_clang'); + await File.fromUri(fakeClang).create(); + fakeLd = tempUri.resolve('fake_ld'); + await File.fromUri(fakeLd).create(); + fakeAr = tempUri.resolve('fake_ar'); + await File.fromUri(fakeAr).create(); + }); + + tearDown(() async { + await Directory.fromUri(tempUri).delete(recursive: true); + }); + + test('BuildConfig ==', () { + final config1 = BuildConfig( + outDir: tempUri.resolve('out1/'), + packageRoot: tempUri, + target: Target.iOSArm64, + targetIOSSdk: IOSSdk.iPhoneOs, + cc: fakeClang, + ld: fakeLd, + ar: fakeAr, + packaging: PackagingPreference.preferStatic, + ); + + final config2 = BuildConfig( + outDir: tempUri.resolve('out2/'), + packageRoot: tempUri, + target: Target.androidArm64, + packaging: PackagingPreference.preferStatic, + ); + + expect(config1, equals(config1)); + expect(config1 == config2, false); + expect(config1.outDir != config2.outDir, true); + expect(config1.packageRoot, config2.packageRoot); + expect(config1.target != config2.target, true); + expect(config1.targetIOSSdk != config2.targetIOSSdk, true); + expect(config1.cc != config2.cc, true); + expect(config1.ld != config2.ld, true); + expect(config1.ar != config2.ar, true); + expect(config1.packaging, config2.packaging); + expect(config1.dependencyMetadata, config2.dependencyMetadata); + }); + + test('BuildConfig fromConfig', () { + final buildConfig2 = BuildConfig( + outDir: tempUri.resolve('out2/'), + packageRoot: tempUri.resolve('packageRoot/'), + target: Target.androidArm64, + packaging: PackagingPreference.preferStatic, + ); + + final config = Config(fileParsed: { + 'out_dir': tempUri.resolve('out2/').path, + 'package_root': tempUri.resolve('packageRoot/').path, + 'target': 'android_arm64', + 'packaging': 'prefer-static', + }); + + final fromConfig = BuildConfig.fromConfig(config); + expect(fromConfig, equals(buildConfig2)); + }); + + test('BuildConfig toYaml fromConfig', () { + final buildConfig1 = BuildConfig( + outDir: tempUri.resolve('out1/'), + packageRoot: tempUri.resolve('packageRoot/'), + target: Target.iOSArm64, + targetIOSSdk: IOSSdk.iPhoneOs, + cc: fakeClang, + ld: fakeLd, + packaging: PackagingPreference.preferStatic, + ); + + final configFile = buildConfig1.toYaml(); + final config = Config(fileParsed: configFile); + final fromConfig = BuildConfig.fromConfig(config); + expect(fromConfig, equals(buildConfig1)); + }); + + test('BuildConfig == dependency metadata', () { + final buildConfig1 = BuildConfig( + outDir: tempUri.resolve('out1/'), + packageRoot: tempUri, + target: Target.androidArm64, + packaging: PackagingPreference.preferStatic, + dependencyMetadata: { + 'bar': Metadata({ + 'key': 'value', + 'foo': ['asdf', 'fdsa'], + }), + 'foo': Metadata({ + 'key': 321, + }), + }, + ); + + final buildConfig2 = BuildConfig( + outDir: tempUri.resolve('out1/'), + packageRoot: tempUri, + target: Target.androidArm64, + packaging: PackagingPreference.preferStatic, + dependencyMetadata: { + 'bar': Metadata({ + 'key': 'value', + }), + 'foo': Metadata({ + 'key': 123, + }), + }, + ); + + expect(buildConfig1, equals(buildConfig1)); + expect(buildConfig1 == buildConfig2, false); + expect(buildConfig1.hashCode == buildConfig2.hashCode, false); + }); + + test('BuildConfig toYaml fromYaml', () { + final outDir = tempUri.resolve('out1/'); + final buildConfig1 = BuildConfig( + outDir: outDir, + packageRoot: tempUri, + target: Target.iOSArm64, + targetIOSSdk: IOSSdk.iPhoneOs, + cc: fakeClang, + ld: fakeLd, + packaging: PackagingPreference.preferStatic, + // This map should be sorted on key for two layers. + dependencyMetadata: { + 'foo': Metadata({ + 'z': ['z', 'a'], + 'a': 321, + }), + 'bar': Metadata({ + 'key': 'value', + }), + }, + ); + final yamlString = buildConfig1.toYamlString(); + final expectedYamlString = '''cc: ${fakeClang.path} +dependency_metadata: + bar: + key: value + foo: + a: 321 + z: + - z + - a +ld: ${fakeLd.path} +out_dir: ${outDir.path} +package_root: ${tempUri.path} +packaging: prefer-static +target: ios_arm64 +target_ios_sdk: iphoneos'''; + expect(yamlString, equals(expectedYamlString)); + + final buildConfig2 = BuildConfig.fromConfig( + Config.fromConfigFileContents( + fileContents: yamlString, + ), + ); + expect(buildConfig2, buildConfig1); + }); + + test('BuildConfig FormatExceptions', () { + expect( + () => BuildConfig.fromConfig(Config(fileParsed: {})), + throwsFormatException, + ); + expect( + () => BuildConfig.fromConfig(Config(fileParsed: { + 'package_root': tempUri.resolve('packageRoot/').path, + 'target': 'android_arm64', + 'packaging': 'prefer-static', + })), + throwsFormatException, + ); + expect( + () => BuildConfig.fromConfig(Config(fileParsed: { + 'out_dir': tempUri.resolve('out2/').path, + 'package_root': tempUri.resolve('packageRoot/').path, + 'target': 'android_arm64', + 'packaging': 'prefer-static', + 'dependency_metadata': { + 'bar': {'key': 'value'}, + 'foo': [], + }, + })), + throwsFormatException, + ); + }); + + test('FormatExceptions contain full stack trace of wrapped exception', () { + try { + BuildConfig.fromConfig(Config(fileParsed: { + 'out_dir': tempUri.resolve('out2/').path, + 'package_root': tempUri.resolve('packageRoot/').path, + 'target': [1, 2, 3, 4, 5], + 'packaging': 'prefer-static', + })); + } on FormatException catch (e) { + expect(e.toString(), stringContainsInOrder(['Config.string'])); + } + }); + + test('BuildConfig toString', () { + final config = BuildConfig( + outDir: tempUri.resolve('out1/'), + packageRoot: tempUri, + target: Target.iOSArm64, + targetIOSSdk: IOSSdk.iPhoneOs, + cc: fakeClang, + ld: fakeLd, + packaging: PackagingPreference.preferStatic, + ); + config.toString(); + }); + + test('BuildConfig fromArgs', () async { + final buildConfig = BuildConfig( + outDir: tempUri.resolve('out2/'), + packageRoot: tempUri, + target: Target.androidArm64, + packaging: PackagingPreference.preferStatic, + ); + final configFileContents = buildConfig.toYamlString(); + final configUri = tempUri.resolve('config.yaml'); + final configFile = File.fromUri(configUri); + await configFile.writeAsString(configFileContents); + final buildConfig2 = + await BuildConfig.fromArgs(['--config', configUri.toFilePath()]); + expect(buildConfig2, buildConfig); + }); + + test('dependency metadata via config accessor', () { + final buildConfig1 = BuildConfig( + outDir: tempUri.resolve('out1/'), + packageRoot: tempUri, + target: Target.androidArm64, + packaging: PackagingPreference.preferStatic, + dependencyMetadata: { + 'bar': Metadata({ + 'key': {'key2': 'value'}, + }), + }, + ); + // Useful for doing `path(..., exists: true)`. + expect( + buildConfig1.config.string([ + BuildConfig.dependencyMetadataConfigKey, + 'bar', + 'key', + 'key2' + ].join('.')), + 'value', + ); + }); +} diff --git a/pkgs/native_assets_cli/test/model/build_output_test.dart b/pkgs/native_assets_cli/test/model/build_output_test.dart new file mode 100644 index 0000000000..6751416b6f --- /dev/null +++ b/pkgs/native_assets_cli/test/model/build_output_test.dart @@ -0,0 +1,97 @@ +// 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:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + late Uri tempUri; + + setUp(() async { + tempUri = (await Directory.systemTemp.createTemp()).uri; + }); + + tearDown(() async { + await Directory.fromUri(tempUri).delete(recursive: true); + }); + + final buildOutput = BuildOutput( + timestamp: DateTime.parse('2022-11-10 13:25:01.000'), + assets: [ + Asset( + name: 'foo', + path: AssetAbsolutePath(Uri(path: 'path/to/libfoo.so')), + target: Target.androidX64, + packaging: Packaging.dynamic, + ), + Asset( + name: 'foo2', + path: AssetRelativePath(Uri(path: 'path/to/libfoo2.so')), + target: Target.androidX64, + packaging: Packaging.dynamic, + ), + ], + dependencies: Dependencies([ + Uri.file('path/to/file.ext'), + ]), + metadata: Metadata({ + 'key': 'value', + }), + ); + + const yamlEncoding = '''timestamp: 2022-11-10 13:25:01.000 +assets: + - name: foo + packaging: dynamic + path: + path_type: absolute + uri: path/to/libfoo.so + target: android_x64 + - name: foo2 + packaging: dynamic + path: + path_type: relative + uri: path/to/libfoo2.so + target: android_x64 +dependencies: + - path/to/file.ext +metadata: + key: value'''; + + test('built info yaml', () { + final yaml = buildOutput.toYamlString().replaceAll('\\', '/'); + expect(yaml, yamlEncoding); + final buildOutput2 = BuildOutput.fromYamlString(yaml); + expect(buildOutput.hashCode, buildOutput2.hashCode); + expect(buildOutput, buildOutput2); + }); + + test('BuildOutput.toString', buildOutput.toString); + + test('BuildOutput.hashCode', () { + final buildOutput2 = BuildOutput.fromYamlString(yamlEncoding); + expect(buildOutput.hashCode, buildOutput2.hashCode); + + final buildOutput3 = BuildOutput( + timestamp: DateTime.parse('2022-11-10 13:25:01.000'), + ); + expect(buildOutput.hashCode != buildOutput3.hashCode, true); + }); + + test('BuildOutput.readFromFile BuildOutput.writeToFile', () async { + final outDir = tempUri.resolve('out_dir/'); + await buildOutput.writeToFile(outDir: outDir); + final buildOutput2 = await BuildOutput.readFromFile(outDir: outDir); + expect(buildOutput2, buildOutput); + }); + + test('Round timestamp', () { + final buildOutput3 = BuildOutput( + timestamp: DateTime.parse('2022-11-10 13:25:01.372257'), + ); + expect(buildOutput3.timestamp, DateTime.parse('2022-11-10 13:25:01.000')); + }); +} diff --git a/pkgs/native_assets_cli/test/model/dependencies_test.dart b/pkgs/native_assets_cli/test/model/dependencies_test.dart new file mode 100644 index 0000000000..e707e949f8 --- /dev/null +++ b/pkgs/native_assets_cli/test/model/dependencies_test.dart @@ -0,0 +1,84 @@ +// 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:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + late Uri tempUri; + + setUp(() async => tempUri = (await Directory.systemTemp.createTemp()).uri); + + tearDown( + () async => await Directory.fromUri(tempUri).delete(recursive: true)); + + final dependencies = Dependencies([ + Uri.file('src/bar.c'), + Uri.file('src/baz.c'), + Uri.directory('src/bla/'), + Uri.file('build.dart'), + ]); + + const yamlEncoding = '''- src/bar.c +- src/baz.c +- src/bla/ +- build.dart'''; + + test('dependencies yaml', () { + final yaml = dependencies.toYamlString().replaceAll('\\', '/'); + expect(yaml, yamlEncoding); + final dependencies2 = Dependencies.fromYamlString(yaml); + expect(dependencies.hashCode, dependencies2.hashCode); + expect(dependencies, dependencies2); + }); + + test('dependencies toString', dependencies.toString); + + test('dependencies fromYamlString', () { + final dependencies = Dependencies.fromYamlString(''); + expect(dependencies, Dependencies([])); + }); + + test('dependencies lastModified', () async { + final dirUri = tempUri.resolve('foo/'); + final dir = Directory.fromUri(dirUri); + await dir.create(); + final fileUri = tempUri.resolve('bla.c'); + final file = File.fromUri(fileUri); + await file.writeAsString('dummy contents'); + final dependencies = Dependencies([dirUri, fileUri]); + expect(await dependencies.lastModified(), await file.lastModified()); + }); + + test('dependencies lastModified symlinks', () async { + final symlink = Link.fromUri(tempUri.resolve('my_link')); + await symlink.create(tempUri.toFilePath()); + + final someFileUri = tempUri.resolve('foo.txt'); + final someFile = File.fromUri(someFileUri); + await someFile.writeAsString('yay!'); + + final dependencies = Dependencies([tempUri]); + expect(await dependencies.lastModified(), await someFile.lastModified()); + }); + + test('dependencies lastModified does not exist', () async { + final someFileUri = tempUri.resolve('foo.txt'); + final someFile = File.fromUri(someFileUri); + await someFile.writeAsString('yay!'); + + final deletedFileUri = tempUri.resolve('bar.txt'); + + final now = DateTime.now(); + + final dependencies = Dependencies([ + someFileUri, + deletedFileUri, + ]); + final depsLastModified = await dependencies.lastModified(); + expect(depsLastModified == now || depsLastModified.isAfter(now), true); + }); +} diff --git a/pkgs/native_assets_cli/test/model/metadata_test.dart b/pkgs/native_assets_cli/test/model/metadata_test.dart new file mode 100644 index 0000000000..30623191ef --- /dev/null +++ b/pkgs/native_assets_cli/test/model/metadata_test.dart @@ -0,0 +1,25 @@ +// 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 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + final metadata = Metadata({ + 'key': 'value', + 'my_list': [1, 2, 3], + 'my_map': { + 3: 4, + 'foo': 'bar', + }, + }); + + test('Metadata toString', metadata.toString); + + test('Metadata toYamlString fromYamlString', () { + final yamlString = metadata.toYamlString(); + final metadata2 = Metadata.fromYamlString(yamlString); + expect(metadata2, metadata); + }); +} diff --git a/pkgs/native_assets_cli/test/native_assets_cli_test.dart b/pkgs/native_assets_cli/test/model/packaging_test.dart similarity index 62% rename from pkgs/native_assets_cli/test/native_assets_cli_test.dart rename to pkgs/native_assets_cli/test/model/packaging_test.dart index a0de8d3ea3..398f760ef1 100644 --- a/pkgs/native_assets_cli/test/native_assets_cli_test.dart +++ b/pkgs/native_assets_cli/test/model/packaging_test.dart @@ -6,15 +6,7 @@ import 'package:native_assets_cli/native_assets_cli.dart'; import 'package:test/test.dart'; void main() { - group('A group of tests', () { - final awesome = Awesome(); - - setUp(() { - // Additional setup goes here. - }); - - test('First Test', () { - expect(awesome.isAwesome, isTrue); - }); + test('Packaging toString', () async { + Packaging.static.toString(); }); } diff --git a/pkgs/native_assets_cli/test/model/target_test.dart b/pkgs/native_assets_cli/test/model/target_test.dart new file mode 100644 index 0000000000..7a6e3ee94f --- /dev/null +++ b/pkgs/native_assets_cli/test/model/target_test.dart @@ -0,0 +1,49 @@ +// 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:ffi'; +import 'dart:io'; + +import 'package:native_assets_cli/native_assets_cli.dart'; +import 'package:test/test.dart'; + +void main() { + test('OS accessors', () async { + expect(OS.android.isDesktop, false); + expect(OS.android.isMobile, true); + }); + + test('OS naming conventions', () async { + expect(OS.android.dylibFileName('foo'), 'libfoo.so'); + expect(OS.android.staticlibFileName('foo'), 'libfoo.a'); + expect(OS.windows.dylibFileName('foo'), 'foo.dll'); + expect(OS.windows.libraryFileName('foo', Packaging.dynamic), 'foo.dll'); + expect(OS.windows.staticlibFileName('foo'), 'foo.lib'); + expect(OS.windows.libraryFileName('foo', Packaging.static), 'foo.lib'); + expect(OS.windows.executableFileName('foo'), 'foo.exe'); + }); + + test('Target current', () async { + final current = Target.current; + expect(current.toString(), Abi.current().toString()); + }); + + test('Target fromDartPlatform', () async { + final current = Target.fromDartPlatform(Platform.version); + expect(current.toString(), Abi.current().toString()); + expect(() => Target.fromDartPlatform('bogus'), throwsFormatException); + expect( + () => Target.fromDartPlatform( + '3.0.0 (be) (Wed Apr 5 14:19:42 2023 +0000) on "myfancyos_ia32"'), + throwsFormatException); + }); + + test('Target cross compilation', () async { + // All hosts can cross compile to Android. + expect( + Target.current.supportedTargetTargets(), contains(Target.androidArm64)); + expect( + Target.macOSArm64.supportedTargetTargets(), contains(Target.iOSArm64)); + }); +}