From 5f88275bb9633f5f267a8ac9a82c7a217ca9c43b Mon Sep 17 00:00:00 2001 From: Varadraj75 Date: Fri, 14 Aug 2026 23:51:52 +0530 Subject: [PATCH] [native_toolchain_c] Inject FileSystem for file system access Adds an optional fileSystem parameter alongside processManager on CBuilder, CLinker, CLibrary and ToolResolvingContext, defaulting to LocalFileSystem, and routes all file system access through it. Glob resolution switches to listFileSystem. Public APIs keep taking Uri, so nothing user-facing changes. Adds a unit test running CBuilder.run against a MemoryFileSystem and a FakeProcessManager, asserting the full command sequence and the outputs written to the fake file system, plus MemoryFileSystem tests for the path and install-location resolvers. Together with the ProcessManager work this completes the seam. Fixes: https://github.com/dart-lang/native/issues/3072 --- pkgs/native_toolchain_c/CHANGELOG.md | 4 + .../lib/src/cbuilder/cbuilder.dart | 15 +- .../lib/src/cbuilder/clibrary.dart | 12 ++ .../lib/src/cbuilder/clinker.dart | 15 +- .../lib/src/cbuilder/compiler_resolver.dart | 12 +- .../lib/src/cbuilder/linker_options.dart | 49 ++++-- .../lib/src/cbuilder/run_cbuilder.dart | 7 + .../lib/src/native_toolchain/android_ndk.dart | 13 +- .../lib/src/native_toolchain/msvc.dart | 18 ++- .../lib/src/native_toolchain/xcode.dart | 11 +- .../lib/src/tool/tool_resolver.dart | 45 ++++-- pkgs/native_toolchain_c/pubspec.yaml | 1 + .../test/cbuilder/cbuilder_unit_test.dart | 143 ++++++++++++++++++ .../test/cbuilder/compiler_resolver_test.dart | 3 + .../test/native_toolchain/xcode_test.dart | 2 + .../test/tool/tool_resolver_test.dart | 57 +++++++ 16 files changed, 352 insertions(+), 55 deletions(-) create mode 100644 pkgs/native_toolchain_c/test/cbuilder/cbuilder_unit_test.dart diff --git a/pkgs/native_toolchain_c/CHANGELOG.md b/pkgs/native_toolchain_c/CHANGELOG.md index 9f84cc0985..29c75e07ec 100644 --- a/pkgs/native_toolchain_c/CHANGELOG.md +++ b/pkgs/native_toolchain_c/CHANGELOG.md @@ -7,6 +7,10 @@ an optional parameter on `CBuilder.run`, `CLinker.run`, `CLibrary.build`, and `CLibrary.link` (defaulting to `LocalProcessManager`), so process invocations can be mocked in tests. ([#3072](https://github.com/dart-lang/native/issues/3072)) +- Access all file system state through a `package:file` `FileSystem`, exposed as + an optional parameter on `CBuilder.run`, `CLinker.run`, `CLibrary.build`, and + `CLibrary.link` (defaulting to `LocalFileSystem`), so file system access can be + mocked in tests. ([#3072](https://github.com/dart-lang/native/issues/3072)) ## 0.19.3 diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart index f57c85f5b5..0778df1aa2 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/cbuilder.dart @@ -2,9 +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. -import 'dart:io'; - import 'package:code_assets/code_assets.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; import 'package:hooks/hooks.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; @@ -133,6 +133,9 @@ class CBuilder extends CTool implements Builder { /// If provided, uses [processManager] to spawn processes. Otherwise, uses a /// [LocalProcessManager] that spawns real processes. /// + /// If provided, uses [fileSystem] to access the file system. Otherwise, uses + /// a [LocalFileSystem] that accesses the real file system. + /// /// If you're using [CBuilder] in a build hook and [CLinker] in a link hook, /// see [CLibrary] to combine them. @override @@ -141,12 +144,14 @@ class CBuilder extends CTool implements Builder { required BuildOutputBuilder output, Logger? logger, ProcessManager? processManager, + FileSystem? fileSystem, List routing = const [ToAppBundle()], LinkModePreference? linkModePreference, Map? defines, }) async { logger ??= createDefaultLogger(); processManager ??= const LocalProcessManager(); + fileSystem ??= const LocalFileSystem(); if (!input.config.buildCodeAssets) { logger.info( 'config.buildAssetTypes did not contain CodeAssets, ' @@ -161,7 +166,7 @@ class CBuilder extends CTool implements Builder { ); final outDir = input.outputDirectory; final packageRoot = input.packageRoot; - await Directory.fromUri(outDir).create(recursive: true); + await fileSystem.directory(outDir).create(recursive: true); final linkMode = getLinkMode( linkModePreference ?? this.linkModePreference ?? @@ -199,6 +204,7 @@ class CBuilder extends CTool implements Builder { codeConfig: input.config.code, logger: logger, processManager: processManager, + fileSystem: fileSystem, sources: sources, includes: includes, forcedIncludes: forcedIncludes, @@ -246,7 +252,8 @@ class CBuilder extends CTool implements Builder { final includeFiles = await Stream.fromIterable(includes) .asyncExpand( - (include) => Directory(include.toFilePath()) + (include) => fileSystem! + .directory(include.toFilePath()) .list(recursive: true) .where((entry) => entry is File) .map((file) => file.uri), diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/clibrary.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/clibrary.dart index b6c34e9f1a..00260f6f3f 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/clibrary.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/clibrary.dart @@ -3,6 +3,8 @@ // BSD-style license that can be found in the LICENSE file. import 'package:code_assets/code_assets.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; import 'package:hooks/hooks.dart'; import 'package:logging/logging.dart'; import 'package:process/process.dart'; @@ -166,11 +168,15 @@ class CLibrary { /// /// If provided, uses [processManager] to spawn processes. Otherwise, uses a /// [LocalProcessManager] that spawns real processes. + /// + /// If provided, uses [fileSystem] to access the file system. Otherwise, uses + /// a [LocalFileSystem] that accesses the real file system. Future build({ required BuildInput input, required BuildOutputBuilder output, Logger? logger, ProcessManager? processManager, + FileSystem? fileSystem, List? routing, LinkModePreference? linkModePreference, Map? defines, @@ -202,6 +208,7 @@ class CLibrary { output: output, logger: logger, processManager: processManager, + fileSystem: fileSystem, routing: routing ?? (input.config.linkingEnabled @@ -239,11 +246,15 @@ class CLibrary { /// /// If provided, uses [processManager] to spawn processes. Otherwise, uses a /// [LocalProcessManager] that spawns real processes. + /// + /// If provided, uses [fileSystem] to access the file system. Otherwise, uses + /// a [LocalFileSystem] that accesses the real file system. Future link({ required LinkInput input, required LinkOutputBuilder output, Logger? logger, ProcessManager? processManager, + FileSystem? fileSystem, LinkerOptions? linkerOptions, LinkModePreference? linkModePreference, Map? defines, @@ -258,6 +269,7 @@ class CLibrary { output: output, logger: logger, processManager: processManager, + fileSystem: fileSystem, linkerOptions: linkerOptions, linkModePreference: linkModePreference, sources: assets.map((a) => a.file!.toFilePath()).toList(), diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart index b61f48b574..3974323c47 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/clinker.dart @@ -2,9 +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. -import 'dart:io'; - import 'package:code_assets/code_assets.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; import 'package:hooks/hooks.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; @@ -67,6 +67,9 @@ class CLinker extends CTool implements Linker { /// If provided, uses [processManager] to spawn processes. Otherwise, uses a /// [LocalProcessManager] that spawns real processes. /// + /// If provided, uses [fileSystem] to access the file system. Otherwise, uses + /// a [LocalFileSystem] that accesses the real file system. + /// /// If you're using [CBuilder] in a build hook and [CLinker] in a link hook, /// see [CLibrary] to combine them. @override @@ -75,6 +78,7 @@ class CLinker extends CTool implements Linker { required LinkOutputBuilder output, Logger? logger, ProcessManager? processManager, + FileSystem? fileSystem, LinkerOptions? linkerOptions, LinkModePreference? linkModePreference, List? sources, @@ -82,6 +86,7 @@ class CLinker extends CTool implements Linker { }) async { logger ??= createDefaultLogger(); processManager ??= const LocalProcessManager(); + fileSystem ??= const LocalFileSystem(); final effectiveLinkerOptions = linkerOptions ?? this.linkerOptions; if (effectiveLinkerOptions != null && effectiveLinkerOptions.skipWholeLibrary) { @@ -90,7 +95,7 @@ class CLinker extends CTool implements Linker { } final outDir = input.outputDirectory; final packageRoot = input.packageRoot; - await Directory.fromUri(outDir).create(recursive: true); + await fileSystem.directory(outDir).create(recursive: true); final linkMode = getLinkMode( linkModePreference ?? this.linkModePreference ?? @@ -117,6 +122,7 @@ class CLinker extends CTool implements Linker { linkerOptions: linkerOptions ?? this.linkerOptions, logger: logger, processManager: processManager, + fileSystem: fileSystem, sources: resolvedSources, includes: includes, frameworks: frameworks, @@ -148,7 +154,8 @@ class CLinker extends CTool implements Linker { } final includeFiles = await Stream.fromIterable(includes) .asyncExpand( - (include) => Directory(include.toFilePath()) + (include) => fileSystem! + .directory(include.toFilePath()) .list(recursive: true) .where((entry) => entry is File) .map((file) => file.uri), diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart index 95cd2798fa..aac52d95c8 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/compiler_resolver.dart @@ -2,9 +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. -import 'dart:io'; - import 'package:code_assets/code_assets.dart'; +import 'package:file/file.dart' show FileSystem; +import 'package:file/local.dart'; import 'package:logging/logging.dart'; import 'package:process/process.dart'; @@ -26,6 +26,7 @@ class CompilerResolver { final CodeConfig codeConfig; final Logger? logger; final ProcessManager processManager; + final FileSystem fileSystem; final OS hostOS; final Architecture hostArchitecture; final ToolResolvingContext context; @@ -34,14 +35,17 @@ class CompilerResolver { required this.codeConfig, required this.logger, ProcessManager? processManager, + FileSystem? fileSystem, OS? hostOS, // Only visible for testing. Architecture? hostArchitecture, // Only visible for testing. }) : processManager = processManager ?? const LocalProcessManager(), + fileSystem = fileSystem ?? const LocalFileSystem(), hostOS = hostOS ?? .current, hostArchitecture = hostArchitecture ?? .current, context = ToolResolvingContext( logger: logger, processManager: processManager, + fileSystem: fileSystem, ); Future resolveCompiler() async { @@ -126,7 +130,7 @@ class CompilerResolver { Future _tryLoadCompilerFromInput() async { final inputCcUri = codeConfig.cCompiler?.compiler; if (inputCcUri != null) { - assert(await File.fromUri(inputCcUri).exists()); + assert(await fileSystem.file(inputCcUri).exists()); logger?.finer( 'Using compiler ${inputCcUri.toFilePath()} ' 'from BuildInput.cCompiler.cc.', @@ -239,7 +243,7 @@ class CompilerResolver { Future _tryLoadArchiverFromInput() async { final inputArUri = codeConfig.cCompiler?.archiver; if (inputArUri != null) { - assert(await File.fromUri(inputArUri).exists()); + assert(await fileSystem.file(inputArUri).exists()); logger?.finer( 'Using archiver ${inputArUri.toFilePath()} ' 'from BuildInput.cCompiler.ar.', diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart index e53543dc74..b8140adcfd 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/linker_options.dart @@ -2,9 +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. -import 'dart:io'; - import 'package:code_assets/code_assets.dart'; +import 'package:file/file.dart' show FileSystem; import '../native_toolchain/msvc.dart'; import '../native_toolchain/tool_likeness.dart'; @@ -104,15 +103,22 @@ extension LinkerOptionsExt on LinkerOptions { Iterable sourceFiles, OS targetOS, Architecture targetArchitecture, + FileSystem fileSystem, ) { if (tool.isClangLike || tool.isLdLike) { - return _sourceFilesToFlagsForClangLike(tool, sourceFiles, targetOS); + return _sourceFilesToFlagsForClangLike( + tool, + sourceFiles, + targetOS, + fileSystem, + ); } else if (tool == cl) { return _sourceFilesToFlagsForCl( tool, sourceFiles, targetOS, targetArchitecture, + fileSystem, ); } else { throw UnimplementedError('This package does not know how to run $tool.'); @@ -123,6 +129,7 @@ extension LinkerOptionsExt on LinkerOptions { Tool tool, Iterable sourceFiles, OS targetOS, + FileSystem fileSystem, ) { switch (targetOS) { case .macOS || .iOS: @@ -137,7 +144,8 @@ extension LinkerOptionsExt on LinkerOptions { if (_linkerScriptMode is ManualLinkerScript) '-exported_symbols_list,${_linkerScriptMode.script.toFilePath()}' else if (_linkerScriptMode is GenerateLinkerScript) - '-exported_symbols_list,${_createMacSymbolList(_symbols)}', + '-exported_symbols_list,' + '${_createMacSymbolList(_symbols, fileSystem)}', ]), ]; @@ -157,7 +165,8 @@ extension LinkerOptionsExt on LinkerOptions { if (_linkerScriptMode is ManualLinkerScript) '--version-script=${_linkerScriptMode.script.toFilePath()}' else if (_linkerScriptMode is GenerateLinkerScript) - '--version-script=${_createClangLikeLinkScript(_symbols)}', + '--version-script=' + '${_createClangLikeLinkScript(_symbols, fileSystem)}', if (wholeArchiveSandwich) '--no-whole-archive', ]), ]; @@ -171,6 +180,7 @@ extension LinkerOptionsExt on LinkerOptions { Iterable sourceFiles, OS targetOS, Architecture targetArch, + FileSystem fileSystem, ) => [ ...sourceFiles, '/link', @@ -182,7 +192,7 @@ extension LinkerOptionsExt on LinkerOptions { if (_linkerScriptMode is ManualLinkerScript) '/DEF:${_linkerScriptMode.script.toFilePath()}' else if (_linkerScriptMode is GenerateLinkerScript) - '/DEF:${_createClLinkScript(_symbols)}', + '/DEF:${_createClLinkScript(_symbols, fileSystem)}', if (stripDebug) '/PDBSTRIPPED', if (gcSections) '/OPT:REF', ]; @@ -193,18 +203,24 @@ extension LinkerOptionsExt on LinkerOptions { /// using `ld -why_live`, see https://www.unix.com/man_page/osx/1/ld/, where /// the reason will show up as `global-dont-strip`. /// This might possibly be a Rust only feature. - static String _createMacSymbolList(Iterable symbols) { - final tempDir = Directory.systemTemp.createTempSync(); + static String _createMacSymbolList( + Iterable symbols, + FileSystem fileSystem, + ) { + final tempDir = fileSystem.systemTempDirectory.createTempSync(); final symbolsFileUri = tempDir.uri.resolve('exported_symbols_list.txt'); - final symbolsFile = File.fromUri(symbolsFileUri)..createSync(); + final symbolsFile = fileSystem.file(symbolsFileUri)..createSync(); symbolsFile.writeAsStringSync(symbols.map((e) => '_$e').join('\n')); return symbolsFileUri.toFilePath(); } - static String _createClangLikeLinkScript(Iterable symbols) { - final tempDir = Directory.systemTemp.createTempSync(); + static String _createClangLikeLinkScript( + Iterable symbols, + FileSystem fileSystem, + ) { + final tempDir = fileSystem.systemTempDirectory.createTempSync(); final symbolsFileUri = tempDir.uri.resolve('symbols.lds'); - final symbolsFile = File.fromUri(symbolsFileUri)..createSync(); + final symbolsFile = fileSystem.file(symbolsFileUri)..createSync(); symbolsFile.writeAsStringSync(''' { global: @@ -216,10 +232,13 @@ extension LinkerOptionsExt on LinkerOptions { return symbolsFileUri.toFilePath(); } - static String _createClLinkScript(Iterable symbols) { - final tempDir = Directory.systemTemp.createTempSync(); + static String _createClLinkScript( + Iterable symbols, + FileSystem fileSystem, + ) { + final tempDir = fileSystem.systemTempDirectory.createTempSync(); final symbolsFileUri = tempDir.uri.resolve('symbols.def'); - final symbolsFile = File.fromUri(symbolsFileUri)..createSync(); + final symbolsFile = fileSystem.file(symbolsFileUri)..createSync(); symbolsFile.writeAsStringSync(''' LIBRARY MyDLL EXPORTS diff --git a/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart b/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart index c3ccfc3db0..5aad3a1aa4 100644 --- a/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart +++ b/pkgs/native_toolchain_c/lib/src/cbuilder/run_cbuilder.dart @@ -6,6 +6,7 @@ import 'dart:io'; import 'dart:math'; import 'package:code_assets/code_assets.dart'; +import 'package:file/file.dart' show FileSystem; import 'package:hooks/hooks.dart'; import 'package:logging/logging.dart'; import 'package:process/process.dart'; @@ -30,6 +31,7 @@ class RunCBuilder { final CodeConfig codeConfig; final Logger? logger; final ProcessManager processManager; + final FileSystem fileSystem; final List sources; final List includes; final List forcedIncludes; @@ -60,6 +62,7 @@ class RunCBuilder { required this.input, required this.codeConfig, required this.processManager, + required this.fileSystem, this.linkerOptions, this.logger, this.sources = const [], @@ -97,6 +100,7 @@ class RunCBuilder { codeConfig: codeConfig, logger: logger, processManager: processManager, + fileSystem: fileSystem, ); Future compiler() async => await _resolver.resolveCompiler(); @@ -264,6 +268,7 @@ class RunCBuilder { final context = ToolResolvingContext( logger: logger, processManager: processManager, + fileSystem: fileSystem, ); String toolPath(Uri uri) => _toolPath(uri, toolInstance); @@ -377,6 +382,7 @@ class RunCBuilder { sourceFiles, codeConfig.targetOS, codeConfig.targetArchitecture, + fileSystem, ) else ...sourceFiles, @@ -451,6 +457,7 @@ class RunCBuilder { sourceFiles, codeConfig.targetOS, codeConfig.targetArchitecture, + fileSystem, ) else ...[ ...sourceFiles, diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/android_ndk.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/android_ndk.dart index bf7c88f2a7..d6bffb97f4 100644 --- a/pkgs/native_toolchain_c/lib/src/native_toolchain/android_ndk.dart +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/android_ndk.dart @@ -2,9 +2,10 @@ // 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 'dart:io' show Platform; import 'package:code_assets/code_assets.dart'; +import 'package:file/file.dart'; import 'package:glob/glob.dart'; import 'package:logging/logging.dart'; import 'package:process/process.dart'; @@ -83,6 +84,7 @@ class _AndroidNdkResolver implements ToolResolver { ndkInstance, logger: context.logger, processManager: context.processManager, + fileSystem: context.fileSystem, ), ], ]; @@ -92,12 +94,13 @@ class _AndroidNdkResolver implements ToolResolver { ToolInstance androidNdkInstance, { required Logger? logger, required ProcessManager processManager, + required FileSystem fileSystem, }) async { final result = []; final prebuiltUri = androidNdkInstance.uri.resolve( 'toolchains/llvm/prebuilt/', ); - final prebuiltDir = Directory.fromUri(prebuiltUri); + final prebuiltDir = fileSystem.directory(prebuiltUri); if (!prebuiltDir.existsSync()) { return []; } @@ -108,7 +111,7 @@ class _AndroidNdkResolver implements ToolResolver { final clangUri = hostArchDir.uri .resolve('bin/') .resolve(OS.current.executableFileName('clang')); - if (await File.fromUri(clangUri).exists()) { + if (await fileSystem.file(clangUri).exists()) { result.add( await CliVersionResolver.lookupVersion( ToolInstance(tool: androidNdkClang, uri: clangUri), @@ -120,7 +123,7 @@ class _AndroidNdkResolver implements ToolResolver { final arUri = hostArchDir.uri .resolve('bin/') .resolve(OS.current.executableFileName('llvm-ar')); - if (await File.fromUri(arUri).exists()) { + if (await fileSystem.file(arUri).exists()) { result.add( await CliVersionResolver.lookupVersion( ToolInstance(tool: androidNdkLlvmAr, uri: arUri), @@ -132,7 +135,7 @@ class _AndroidNdkResolver implements ToolResolver { final ldUri = hostArchDir.uri .resolve('bin/') .resolve(OS.current.executableFileName('ld.lld')); - if (await File.fromUri(arUri).exists()) { + if (await fileSystem.file(arUri).exists()) { result.add( await CliVersionResolver.lookupVersion( ToolInstance(tool: androidNdkLld, uri: ldUri), diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart index 7bb970a8f1..d5bb139362 100644 --- a/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/msvc.dart @@ -3,9 +3,9 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:convert'; -import 'dart:io'; import 'package:code_assets/code_assets.dart'; +import 'package:file/file.dart' show FileSystem; import 'package:glob/glob.dart'; import 'package:logging/logging.dart'; @@ -270,20 +270,30 @@ class VisualStudioResolver implements ToolResolver { logger: logger, processManager: context.processManager, ); - final instances = parseVswhere(vswhereResult.stdout, logger); + final instances = parseVswhere( + vswhereResult.stdout, + context.fileSystem, + logger, + ); result.addAll(instances); } return result; } - List parseVswhere(String vswhereStdout, [Logger? logger]) { + List parseVswhere( + String vswhereStdout, + FileSystem fileSystem, [ + Logger? logger, + ]) { final result = []; final toolInfos = json.decode(vswhereStdout) as List; for (final toolInfo in toolInfos) { final toolInfoParsed = toolInfo as Map; if (toolInfoParsed['installationPath'] != null && toolInfoParsed['installationVersion'] != null) { - final dir = Directory(toolInfoParsed['installationPath'] as String); + final dir = fileSystem.directory( + toolInfoParsed['installationPath'] as String, + ); assert(dir.existsSync()); final uri = dir.uri; final version = versionFromString( diff --git a/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart b/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart index d480c2e530..1029d4b11f 100644 --- a/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart +++ b/pkgs/native_toolchain_c/lib/src/native_toolchain/xcode.dart @@ -2,8 +2,7 @@ // 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:file/file.dart' show FileSystem; import 'package:logging/logging.dart'; import 'package:process/process.dart'; @@ -56,6 +55,7 @@ class XCodeSdkResolver implements ToolResolver { tool: macosxSdk, logger: context.logger, processManager: context.processManager, + fileSystem: context.fileSystem, ), ...await tryResolveSdk( xcrunInstance: xcrunInstance, @@ -63,6 +63,7 @@ class XCodeSdkResolver implements ToolResolver { tool: iPhoneOSSdk, logger: context.logger, processManager: context.processManager, + fileSystem: context.fileSystem, ), ...await tryResolveSdk( xcrunInstance: xcrunInstance, @@ -70,6 +71,7 @@ class XCodeSdkResolver implements ToolResolver { tool: iPhoneSimulatorSdk, logger: context.logger, processManager: context.processManager, + fileSystem: context.fileSystem, ), ], // xcrun --sdk macosx --show-sdk-path) @@ -82,6 +84,7 @@ class XCodeSdkResolver implements ToolResolver { required Tool tool, required Logger? logger, required ProcessManager processManager, + required FileSystem fileSystem, }) async { final result = await runProcess( executable: xcrunInstance.uri, @@ -98,12 +101,12 @@ class XCodeSdkResolver implements ToolResolver { final uriSymbolic = Uri.directory(result.stdout.trim()); logger?.fine('Found $sdk at ${uriSymbolic.toFilePath()}'); final uri = Uri.directory( - await Directory.fromUri(uriSymbolic).resolveSymbolicLinks(), + await fileSystem.directory(uriSymbolic).resolveSymbolicLinks(), ); if (uriSymbolic != uri) { logger?.fine('Found $sdk at ${uri.toFilePath()}'); } - assert(await Directory.fromUri(uri).exists()); + assert(await fileSystem.directory(uri).exists()); return [ToolInstance(tool: tool, uri: uri)]; } } diff --git a/pkgs/native_toolchain_c/lib/src/tool/tool_resolver.dart b/pkgs/native_toolchain_c/lib/src/tool/tool_resolver.dart index 2b32c8f208..9dc1408320 100644 --- a/pkgs/native_toolchain_c/lib/src/tool/tool_resolver.dart +++ b/pkgs/native_toolchain_c/lib/src/tool/tool_resolver.dart @@ -3,11 +3,12 @@ // BSD-style license that can be found in the LICENSE file. import 'dart:convert'; -import 'dart:io'; +import 'dart:io' show Platform; import 'package:code_assets/code_assets.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; import 'package:glob/glob.dart'; -import 'package:glob/list_local_fs.dart'; import 'package:logging/logging.dart'; import 'package:process/process.dart'; import 'package:pub_semver/pub_semver.dart'; @@ -29,21 +30,23 @@ abstract class ToolResolver { /// global state not available in this context. Not all resolvers adhere to that /// though, since some need to run subprocesses to resolve tools. final class ToolResolvingContext { - // TODO: Expose a package:file file system here and use it in resolvers to - // consistently mock external file system state. - final Logger? logger; final Map environment; /// Used to spawn processes so they can be mocked in tests. final ProcessManager processManager; + /// Used to access the file system so it can be mocked in tests. + final FileSystem fileSystem; + ToolResolvingContext({ required this.logger, Map? environment, ProcessManager? processManager, + FileSystem? fileSystem, }) : environment = environment ?? Platform.environment, - processManager = processManager ?? const LocalProcessManager(); + processManager = processManager ?? const LocalProcessManager(), + fileSystem = fileSystem ?? const LocalFileSystem(); } /// Tries to resolve a tool on the `PATH`. @@ -91,8 +94,12 @@ class PathToolResolver extends ToolResolver { processManager: context.processManager, ); if (process.exitCode == 0) { - final file = File(LineSplitter.split(process.stdout).first); - final uri = File(await file.resolveSymbolicLinks()).uri; + final file = context.fileSystem.file( + LineSplitter.split(process.stdout).first, + ); + final uri = context.fileSystem + .file(await file.resolveSymbolicLinks()) + .uri; if (uri.pathSegments.last case 'llvm' || 'lld') { // https://github.com/dart-lang/native/issues/136 return file.uri; @@ -236,7 +243,7 @@ class InstallLocationResolver implements ToolResolver { final logger = context.logger; logger?.finer('Looking for $toolName in $paths.'); final resolvedPaths = [ - for (final path in paths) ...await tryResolvePath(path), + for (final path in paths) ...await tryResolvePath(path, context), ]; final toolInstances = [ for (final uri in resolvedPaths) @@ -253,7 +260,10 @@ class InstallLocationResolver implements ToolResolver { return toolInstances; } - Future> tryResolvePath(String path) async { + Future> tryResolvePath( + String path, + ToolResolvingContext context, + ) async { if (path.startsWith(home)) { final homeDir_ = homeDir; if (homeDir_ == null) return []; @@ -264,7 +274,9 @@ class InstallLocationResolver implements ToolResolver { } final result = []; - final fileSystemEntities = await Glob(path).list().toList(); + final fileSystemEntities = await Glob( + path, + ).listFileSystem(context.fileSystem).toList(); for (final fileSystemEntity in fileSystemEntities) { if (!await fileSystemEntity.exists()) { continue; @@ -281,7 +293,9 @@ class InstallLocationResolver implements ToolResolver { final path = Platform.environment['HOME'] ?? Platform.environment['USERPROFILE']; if (path == null) return null; - return Directory(path).uri; + // Pure path-to-URI derivation of the host home directory; no file system + // access, so it does not need the mockable file system from the context. + return Uri.directory(path); }(); } @@ -309,10 +323,10 @@ class EnvironmentVariableResolver implements ToolResolver { logger?.fine('Looking for $toolName in environment variable $key'); if (context.environment[key] case final found?) { final fileSystemEntities = switch (glob) { - null => [Directory(found)], + null => [context.fileSystem.directory(found)], final glob => await glob - .list(root: found) + .listFileSystem(context.fileSystem, root: found) .where( // If the path ends in /, only consider directories (entity) => @@ -372,7 +386,8 @@ class RelativeToolResolver implements ToolResolver { ), ]; final fileSystemEntities = [ - for (final glob in globs) ...await glob.list().toList(), + for (final glob in globs) + ...await glob.listFileSystem(context.fileSystem).toList(), ]; final result = [ diff --git a/pkgs/native_toolchain_c/pubspec.yaml b/pkgs/native_toolchain_c/pubspec.yaml index 3f19225206..8eff921c79 100644 --- a/pkgs/native_toolchain_c/pubspec.yaml +++ b/pkgs/native_toolchain_c/pubspec.yaml @@ -18,6 +18,7 @@ environment: dependencies: code_assets: ^2.0.0-wip + file: ^7.0.1 glob: ^2.1.1 hooks: ^2.0.0 logging: ^1.3.0 diff --git a/pkgs/native_toolchain_c/test/cbuilder/cbuilder_unit_test.dart b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_unit_test.dart new file mode 100644 index 0000000000..65b112c0a3 --- /dev/null +++ b/pkgs/native_toolchain_c/test/cbuilder/cbuilder_unit_test.dart @@ -0,0 +1,143 @@ +// Copyright (c) 2026, 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:code_assets/code_assets.dart'; +import 'package:file/memory.dart'; +import 'package:hooks/hooks.dart'; +import 'package:native_toolchain_c/native_toolchain_c.dart'; +import 'package:test/test.dart'; + +import '../helpers.dart'; +import '../utils/fake_process_manager.dart'; + +void main() { + // An end-to-end run of [CBuilder.run] with both external seams faked: a + // [MemoryFileSystem] for all file system access native_toolchain_c performs, + // and a [FakeProcessManager] scripting the compiler resolution and the + // compile invocation. No real compiler is invoked and no real files are + // touched by native_toolchain_c, so this test is host-independent. + // + // Note: [BuildInput.outputDirectory] (from package:hooks) still creates its + // directory on the real file system, so [tempDirForTest] is used for the + // shared output directory, exactly as the integration tests do. Everything + // native_toolchain_c itself touches goes through the [MemoryFileSystem]. + test('CBuilder.run with faked file system and process manager', () async { + final fileSystem = MemoryFileSystem( + style: Platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix, + ); + + // The shared output directory has to live on the real file system because + // `input.outputDirectory` (package:hooks) hardcodes `dart:io`. + final outputDirectoryShared = await tempDirForTest(); + final packageRoot = await tempDirForTest(); + + const name = 'mylib'; + const targetOS = OS.linux; + const targetArchitecture = Architecture.x64; + + // A deterministic toolchain provided via the input config, so compiler + // resolution does not depend on the host machine. The file has to exist in + // the (memory) file system, because the compiler resolver asserts it does. + // Naming it with the host `executableFileName` keeps the compiler + // recognizer (which uses `OS.current`) working on every host. + final ccUri = packageRoot.resolve(OS.current.executableFileName('clang')); + final arUri = packageRoot.resolve(OS.current.executableFileName('llvm-ar')); + final ldUri = packageRoot.resolve(OS.current.executableFileName('ld.lld')); + for (final toolUri in [ccUri, arUri, ldUri]) { + fileSystem.file(toolUri) + ..createSync(recursive: true) + ..writeAsStringSync(''); + } + + // A fake C source file in the memory file system. + final sourceUri = packageRoot.resolve('src.c'); + fileSystem.file(sourceUri) + ..createSync(recursive: true) + ..writeAsStringSync('int foo() { return 0; }\n'); + + final ccPath = ccUri.toFilePath(); + + // Script the process invocations: two `clang --version` calls (one by the + // compiler recognizer, one by the version lookup), then the compile itself. + // The compile's `onRun` creates the expected output library in the memory + // file system so any downstream exists-checks are satisfied. + const versionStdout = 'clang version 14.0.0\n'; + final fakeProcessManager = FakeProcessManager([ + FakeCommand(command: [ccPath, '--version'], stdout: versionStdout), + FakeCommand(command: [ccPath, '--version'], stdout: versionStdout), + FakeCommand( + onRun: (command) { + final outIndex = command.indexOf('-o'); + expect(outIndex, greaterThanOrEqualTo(0)); + fileSystem.file(command[outIndex + 1]).createSync(recursive: true); + }, + ), + ]); + + final buildInputBuilder = BuildInputBuilder() + ..setupShared( + packageName: name, + packageRoot: packageRoot, + outputFile: outputDirectoryShared.resolve('output.json'), + outputDirectoryShared: outputDirectoryShared, + ) + ..config.setupBuild(linkingEnabled: false) + ..addExtension( + CodeAssetExtension( + targetOS: targetOS, + targetArchitecture: targetArchitecture, + linkModePreference: LinkModePreference.dynamic, + cCompiler: CCompilerConfig( + archiver: arUri, + compiler: ccUri, + linker: ldUri, + ), + ), + ); + final buildInput = buildInputBuilder.build(); + final buildOutput = BuildOutputBuilder(); + + final cbuilder = CBuilder.library( + name: name, + assetName: name, + sources: [sourceUri.toFilePath()], + ); + + await cbuilder.run( + input: buildInput, + output: buildOutput, + logger: logger, + processManager: fakeProcessManager, + fileSystem: fileSystem, + ); + + // Every scripted command was consumed, in order. + expect(fakeProcessManager.allCommandsConsumed, true); + final invocations = fakeProcessManager.invocations; + expect(invocations.length, 3); + expect(invocations[0].command, [ccPath, '--version']); + expect(invocations[1].command, [ccPath, '--version']); + + // The compile invocation runs the provided compiler on the source file and + // writes the shared library. + final compileCommand = invocations[2].command; + expect(compileCommand.first, ccPath); + expect(compileCommand, contains(sourceUri.toFilePath())); + expect(compileCommand, contains('--shared')); + final outIndex = compileCommand.indexOf('-o'); + expect(outIndex, greaterThanOrEqualTo(0)); + final outputLibraryPath = compileCommand[outIndex + 1]; + + // The scripted output exists in the memory file system. + expect(fileSystem.file(outputLibraryPath).existsSync(), true); + expect( + outputLibraryPath, + endsWith(targetOS.libraryFileName(name, DynamicLoadingBundled())), + ); + }); +} diff --git a/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart b/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart index cf0e284824..856b3d2176 100644 --- a/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart +++ b/pkgs/native_toolchain_c/test/cbuilder/compiler_resolver_test.dart @@ -7,6 +7,7 @@ library; import 'package:code_assets/code_assets.dart'; import 'package:collection/collection.dart'; +import 'package:file/local.dart'; import 'package:hooks/hooks.dart'; import 'package:native_toolchain_c/src/cbuilder/compiler_resolver.dart'; import 'package:native_toolchain_c/src/native_toolchain/apple_clang.dart'; @@ -78,6 +79,7 @@ void main() { codeConfig: buildInput.config.code, logger: logger, processManager: const LocalProcessManager(), + fileSystem: const LocalFileSystem(), ); final compiler = await resolver.resolveCompiler(); final archiver = await resolver.resolveArchiver(); @@ -117,6 +119,7 @@ void main() { codeConfig: buildInput.config.code, logger: logger, processManager: const LocalProcessManager(), + fileSystem: const LocalFileSystem(), hostOS: .android, // This is never a host. hostArchitecture: .arm64, // This is never a host. ); diff --git a/pkgs/native_toolchain_c/test/native_toolchain/xcode_test.dart b/pkgs/native_toolchain_c/test/native_toolchain/xcode_test.dart index a25d0bdd7e..64f1fd94c4 100644 --- a/pkgs/native_toolchain_c/test/native_toolchain/xcode_test.dart +++ b/pkgs/native_toolchain_c/test/native_toolchain/xcode_test.dart @@ -8,6 +8,7 @@ library; import 'dart:io'; +import 'package:file/local.dart'; import 'package:native_toolchain_c/src/native_toolchain/xcode.dart'; import 'package:native_toolchain_c/src/tool/tool.dart'; import 'package:native_toolchain_c/src/tool/tool_instance.dart'; @@ -61,6 +62,7 @@ void main() { tool: tool, logger: logger, processManager: const LocalProcessManager(), + fileSystem: const LocalFileSystem(), ); expect(result, []); }); diff --git a/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart b/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart index a5a07c0fee..cea1abc8de 100644 --- a/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart +++ b/pkgs/native_toolchain_c/test/tool/tool_resolver_test.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:code_assets/code_assets.dart'; +import 'package:file/memory.dart'; import 'package:native_toolchain_c/src/native_toolchain/apple_clang.dart'; import 'package:native_toolchain_c/src/native_toolchain/clang.dart'; import 'package:native_toolchain_c/src/native_toolchain/msvc.dart'; @@ -16,6 +17,7 @@ import 'package:process/process.dart'; import 'package:test/test.dart'; import '../helpers.dart'; +import '../utils/fake_process_manager.dart'; void main() { test('CliVersionResolver.executableVersion', () async { @@ -118,4 +120,59 @@ void main() { expect(barLogs.join('\n'), contains('Found [ToolInstance(bar')); expect(bazLogs.join('\n'), contains('Found no baz')); }); + + test('PathToolResolver with memory file system', () async { + final fileSystem = MemoryFileSystem( + style: Platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix, + ); + // A tool laid out in the memory file system, found via a scripted `which` + // (`where.exe` on Windows) invocation. + final dir = fileSystem.systemTempDirectory.createTempSync(); + final toolFile = dir.childFile(OS.current.executableFileName('mytool')) + ..createSync(); + + final executableName = OS.current.executableFileName('mytool'); + final fakeProcessManager = FakeProcessManager([ + FakeCommand( + command: [PathToolResolver.which.toFilePath(), executableName], + stdout: '${toolFile.path}\n', + ), + ]); + final context = ToolResolvingContext( + logger: logger, + processManager: fakeProcessManager, + fileSystem: fileSystem, + ); + + final resolved = await PathToolResolver( + toolName: 'mytool', + ).resolve(context); + expect(fakeProcessManager.allCommandsConsumed, true); + expect(resolved.single.uri, toolFile.uri); + }); + + test('InstallLocationResolver with memory file system', () async { + final fileSystem = MemoryFileSystem( + style: Platform.isWindows + ? FileSystemStyle.windows + : FileSystemStyle.posix, + ); + // A tool laid out in the memory file system, found by globbing. + final dir = fileSystem.systemTempDirectory.createTempSync(); + final barFile = dir.childFile(OS.current.executableFileName('bar')) + ..createSync(); + final context = ToolResolvingContext( + logger: logger, + fileSystem: fileSystem, + ); + + final resolver = InstallLocationResolver( + toolName: 'bar', + paths: [barFile.path.replaceAll('\\', '/')], + ); + final resolved = await resolver.resolve(context); + expect(resolved.single.uri, barFile.uri); + }); }