| // 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:hooks/hooks.dart'; | |
| import 'package:logging/logging.dart'; | |
| import 'package:native_toolchain_c/native_toolchain_c.dart'; | |
| import 'build_options.dart'; | |
| import 'fetch.dart'; | |
| import 'logging.dart'; | |
| import 'release_config.dart'; | |
| import 'source_builders.dart'; | |
| import 'symbols_resolver.dart'; | |
| /// Declarative specification for building, fetching, and tree-shaking a native | |
| /// library across `hook/build.dart`, `hook/link.dart`, and standalone | |
| /// maintainer scripts. | |
| /// | |
| /// Modeled after `CLibrary` in `package:native_toolchain_c`, a single | |
| /// [PrebuiltLibrary] instance can be defined in `lib/src/hook_helpers/` and | |
| /// invoked from: | |
| /// - `hook/build.dart` via [build] | |
| /// - `hook/link.dart` via [link] | |
| /// - `tool/precompile_binaries.dart` via [buildStandalone] | |
| class PrebuiltLibrary { | |
| /// The library stem name passed to `OS.dylibFileName` and `CLinker.library` | |
| /// (e.g. `'my_lib'` for `libmy_lib.so`). | |
| final String name; | |
| /// Optional package name override. Defaults to `input.packageName`. | |
| final String? packageName; | |
| /// The `@Native` asset ID suffix (e.g. `'my_package.dart'` or | |
| /// `'src/bindings/lib.g.dart'`). | |
| final String assetName; | |
| /// Configuration for fetching and verifying prebuilt release binaries. | |
| final PrebuiltReleaseConfig? releaseConfig; | |
| /// Optional directory relative to `input.packageRoot` containing pub-bundled | |
| /// prebuilt binaries (e.g. `'prebuilt'`, following `prebuilt_assets_example` | |
| /// in `package:hooks`). | |
| final String? prebuiltDirectory; | |
| /// Callback to compile the native library from source when `buildMode` is | |
| /// `build` or when `fetch` falls back to building from source. | |
| final SourceBuildCallback? buildFromSource; | |
| /// Whether `fetch` mode should fall back to [buildFromSource] if no prebuilt | |
| /// binary is available for the target. | |
| final bool fallbackToBuildOnFetchFailure; | |
| /// Whether unrecognized `buildMode`/`treeshake` values in | |
| /// `hooks.user_defines` throw a [BuildError] (the default) instead of | |
| /// silently using the defaults. | |
| final bool strictBuildOptions; | |
| /// Extracts the native symbols used by the application from | |
| /// `LinkInput.recordedUses`. | |
| /// | |
| /// If `null`, the library is never tree-shaken: [build] bundles the dynamic | |
| /// library directly. | |
| final SymbolsResolver? usedSymbols; | |
| /// Optional callback returning the system libraries to link against in | |
| /// [link] for the target [CodeConfig] (passed as `-l<name>`, or `<name>.lib` | |
| /// for MSVC). | |
| /// | |
| /// Static libraries don't record their dependencies, so this has to list | |
| /// every library that the static library needs and that the linker doesn't | |
| /// link by default (such as `ws2_32` on Windows or `m` on Android). | |
| final List<String> Function(CodeConfig code)? libraries; | |
| /// Optional callback returning the frameworks to link against in [link] for | |
| /// the target [CodeConfig] (passed as `-framework <name>`). | |
| /// | |
| /// Only used when targeting macOS or iOS. If `null`, uses the default of | |
| /// `CLinker` in `package:native_toolchain_c` (`Foundation`). | |
| final List<String> Function(CodeConfig code)? frameworks; | |
| /// Optimization level passed to `CLinker.library` in [link]. | |
| final OptimizationLevel optimizationLevel; | |
| const PrebuiltLibrary({ | |
| required this.name, | |
| this.packageName, | |
| required this.assetName, | |
| this.releaseConfig, | |
| this.prebuiltDirectory, | |
| this.buildFromSource, | |
| this.fallbackToBuildOnFetchFailure = true, | |
| this.strictBuildOptions = true, | |
| this.usedSymbols, | |
| this.libraries, | |
| this.frameworks, | |
| this.optimizationLevel = OptimizationLevel.o3, | |
| }); | |
| /// Runs the build hook (`hook/build.dart`) for this library. | |
| /// | |
| /// When linking is enabled (`input.config.linkingEnabled`), `buildMode` is | |
| /// not [NativeBuildMode.local], `treeshake` is not [TreeshakeMode.off], and | |
| /// [usedSymbols] is set, obtains a static library and routes it to this | |
| /// package's link hook. Otherwise obtains a dynamic library and bundles it | |
| /// directly. | |
| /// | |
| /// In `fetch` mode, if no static library is available for the target and | |
| /// `treeshake` is [TreeshakeMode.auto], falls back to bundling the prebuilt | |
| /// dynamic library (with a warning) before trying [buildFromSource]. | |
| Future<void> build({ | |
| required BuildInput input, | |
| required BuildOutputBuilder output, | |
| List<Uri> additionalDependencies = const [], | |
| Logger? logger, | |
| }) async { | |
| final log = logger ?? defaultLogger; | |
| final pkg = packageName ?? input.packageName; | |
| if (!input.config.buildCodeAssets) { | |
| log.info( | |
| '$pkg: skipping native asset build (code assets not requested).', | |
| ); | |
| return; | |
| } | |
| final buildOptions = _buildOptions(input, pkg); | |
| log.info('$pkg: $buildOptions'); | |
| final treeshake = | |
| input.config.linkingEnabled && | |
| buildOptions.buildMode != NativeBuildMode.local && | |
| buildOptions.treeshake != TreeshakeMode.off; | |
| if (treeshake && | |
| usedSymbols == null && | |
| buildOptions.treeshake == TreeshakeMode.on) { | |
| throw BuildError(message: _noUsedSymbolsMessage(pkg)); | |
| } | |
| // Without `usedSymbols`, nothing can be tree-shaken. | |
| final static = treeshake && usedSymbols != null; | |
| switch (buildOptions.buildMode) { | |
| case NativeBuildMode.fetch: | |
| await _fetchOrFallback( | |
| input, | |
| output, | |
| pkg: pkg, | |
| static: static, | |
| options: buildOptions, | |
| log: log, | |
| ); | |
| case NativeBuildMode.build: | |
| final builtUri = await _requireBuildFromSource( | |
| input, | |
| output, | |
| pkg: pkg, | |
| static: static, | |
| checkoutPath: buildOptions.checkoutPath, | |
| ); | |
| _addLibrary(input, output, pkg: pkg, library: builtUri, static: static); | |
| case NativeBuildMode.local: | |
| await _useLocalBinary( | |
| input, | |
| output, | |
| pkg: pkg, | |
| localPath: buildOptions.localPath, | |
| ); | |
| } | |
| output.dependencies.addAll([ | |
| input.packageRoot.resolve('pubspec.yaml'), | |
| ...additionalDependencies, | |
| ]); | |
| } | |
| BuildOptions _buildOptions(HookInput input, String pkg) => | |
| BuildOptions.fromDefines( | |
| input.userDefines, | |
| packageName: pkg, | |
| strict: strictBuildOptions, | |
| ); | |
| Future<void> _fetchOrFallback( | |
| BuildInput input, | |
| BuildOutputBuilder output, { | |
| required String pkg, | |
| required bool static, | |
| required BuildOptions options, | |
| required Logger log, | |
| }) async { | |
| final config = releaseConfig; | |
| if (config != null) { | |
| final library = await _fetch(input, config, static: static, log: log); | |
| if (library != null) { | |
| _addLibrary(input, output, pkg: pkg, library: library, static: static); | |
| return; | |
| } | |
| // Tree-shaking only makes the library smaller, so in `auto` mode a | |
| // missing static library should not prevent using the dynamic one. | |
| if (static && options.treeshake == TreeshakeMode.auto) { | |
| final dynamic = await _fetch(input, config, static: false, log: log); | |
| if (dynamic != null) { | |
| log.warning( | |
| 'Warning: package:$pkg has no pre-built static library for ' | |
| '${_target(input)} in the ${config.version} release, so it ' | |
| 'bundles the pre-built dynamic library instead, which is not ' | |
| 'tree-shaken and therefore larger.', | |
| ); | |
| _addLibrary( | |
| input, | |
| output, | |
| pkg: pkg, | |
| library: dynamic, | |
| static: false, | |
| ); | |
| return; | |
| } | |
| } | |
| } | |
| if (buildFromSource != null && | |
| (config == null || fallbackToBuildOnFetchFailure)) { | |
| if (config != null) { | |
| log.info('$pkg: falling back to building from source.'); | |
| } | |
| final builtUri = await buildFromSource!( | |
| input, | |
| output, | |
| static: static, | |
| checkoutPath: options.checkoutPath, | |
| ); | |
| _addLibrary(input, output, pkg: pkg, library: builtUri, static: static); | |
| return; | |
| } | |
| throw BuildError( | |
| message: config == null | |
| ? '$pkg: neither `releaseConfig` nor `buildFromSource` is ' | |
| 'configured on PrebuiltLibrary.' | |
| : '$pkg: failed to fetch a pre-built ' | |
| '${static ? 'static' : 'dynamic'} library for ' | |
| '${_target(input)}.', | |
| ); | |
| } | |
| Future<Uri?> _fetch( | |
| HookInput input, | |
| PrebuiltReleaseConfig config, { | |
| required bool static, | |
| required Logger log, | |
| }) => fetchPrebuiltLibrary( | |
| input, | |
| config, | |
| static: static, | |
| prebuiltDirectory: prebuiltDirectory, | |
| canBuildFromSource: buildFromSource != null, | |
| logger: log, | |
| ); | |
| Future<Uri> _requireBuildFromSource( | |
| BuildInput input, | |
| BuildOutputBuilder output, { | |
| required String pkg, | |
| required bool static, | |
| required Uri? checkoutPath, | |
| }) async { | |
| final callback = buildFromSource; | |
| if (callback == null) { | |
| throw BuildError( | |
| message: | |
| '$pkg: `buildMode: build` requires building from source, but ' | |
| 'this package does not support building from source.', | |
| ); | |
| } | |
| return callback(input, output, static: static, checkoutPath: checkoutPath); | |
| } | |
| Future<void> _useLocalBinary( | |
| BuildInput input, | |
| BuildOutputBuilder output, { | |
| required String pkg, | |
| required Uri? localPath, | |
| }) async { | |
| if (localPath == null) { | |
| throw BuildError( | |
| message: | |
| 'buildMode is set to `local`, but `localPath` was not specified ' | |
| 'under `hooks.user_defines.$pkg`.', | |
| ); | |
| } | |
| final file = File.fromUri(localPath); | |
| if (!file.existsSync()) { | |
| throw BuildError( | |
| message: | |
| 'Specified local binary does not exist at ' | |
| '${localPath.toFilePath()}', | |
| ); | |
| } | |
| final dylibFileName = input.config.code.targetOS.dylibFileName(name); | |
| final destFile = File.fromUri(input.outputDirectory.resolve(dylibFileName)); | |
| await destFile.parent.create(recursive: true); | |
| await file.copy(destFile.path); | |
| _addLibrary( | |
| input, | |
| output, | |
| pkg: pkg, | |
| library: destFile.uri, | |
| static: false, | |
| ); | |
| output.dependencies.add(localPath); | |
| } | |
| void _addLibrary( | |
| BuildInput input, | |
| BuildOutputBuilder output, { | |
| required String pkg, | |
| required Uri library, | |
| required bool static, | |
| }) { | |
| output.assets.code.add( | |
| CodeAsset( | |
| package: pkg, | |
| name: assetName, | |
| linkMode: static ? StaticLinking() : DynamicLoadingBundled(), | |
| file: library, | |
| ), | |
| routing: static ? ToLinkHook(input.packageName) : const ToAppBundle(), | |
| ); | |
| } | |
| /// Runs the link hook (`hook/link.dart`) to link and tree-shake the static | |
| /// library emitted by [build] into a dynamic library containing only the | |
| /// functions referenced in `input.recordedUses`. If the application uses | |
| /// none of them, no library is bundled. | |
| /// | |
| /// If record use is disabled (`input.recordedUses` is `null`), it is unknown | |
| /// which functions the application uses, so nothing can be tree-shaken. Then | |
| /// [link] bundles the prebuilt dynamic library in [NativeBuildMode.fetch] if | |
| /// available. Otherwise it links the static library keeping all functions, | |
| /// except on Windows, where it throws a [BuildError] because a DLL only | |
| /// exports the functions it lists. | |
| /// | |
| /// All other assets sent to this link hook are forwarded unchanged. | |
| /// | |
| /// Behavior is controlled by `hooks.user_defines.<package>.treeshake`: | |
| /// - [TreeshakeMode.auto] (default): Tries to tree-shake, and if linking | |
| /// fails in [NativeBuildMode.fetch], prints a warning and falls back to | |
| /// bundling the prebuilt dynamic library. | |
| /// - [TreeshakeMode.on]: Always tries to tree-shake, and throws if record use | |
| /// is disabled or linking fails. | |
| /// - [TreeshakeMode.off]: Never tree-shakes. [build] then bundles the | |
| /// dynamic library directly; if a static library still reaches [link], the | |
| /// prebuilt dynamic library is bundled instead. | |
| Future<void> link({ | |
| required LinkInput input, | |
| required LinkOutputBuilder output, | |
| Logger? logger, | |
| }) async { | |
| final log = logger ?? defaultLogger; | |
| final pkg = packageName ?? input.packageName; | |
| final expectedId = 'package:$pkg/$assetName'; | |
| CodeAsset? staticLibrary; | |
| for (final encoded in input.assets.encodedAssets) { | |
| if (staticLibrary == null && encoded.isCodeAsset) { | |
| final asset = CodeAsset.fromEncoded(encoded); | |
| if (asset.id == expectedId && asset.linkMode is StaticLinking) { | |
| staticLibrary = asset; | |
| continue; | |
| } | |
| } | |
| output.assets.addEncodedAsset(encoded); | |
| } | |
| if (staticLibrary == null) { | |
| // hook/build.dart bundled a dynamic library directly. | |
| return; | |
| } | |
| final buildOptions = _buildOptions(input, pkg); | |
| if (buildOptions.treeshake == TreeshakeMode.off) { | |
| log.info('$pkg: treeshake is off, skipping C linker.'); | |
| if (await _bundlePrebuiltDynamicLibrary(input, output, pkg, log)) { | |
| return; | |
| } | |
| throw BuildError( | |
| message: | |
| '$pkg: treeshake is off, but no pre-built dynamic library is ' | |
| 'available for ${_target(input)} to bundle instead of the static ' | |
| 'library.', | |
| ); | |
| } | |
| final staticLibraryFile = staticLibrary.file!; | |
| final recordedUses = input.recordedUses; | |
| final resolver = usedSymbols; | |
| final List<String>? symbols; | |
| if (recordedUses == null || resolver == null) { | |
| // It's unknown which functions the application uses, so nothing can be | |
| // tree-shaken. [build] only routes a static library here if there is a | |
| // `usedSymbols`, so record use is disabled. | |
| if (buildOptions.treeshake == TreeshakeMode.on) { | |
| throw BuildError( | |
| message: resolver == null | |
| ? _noUsedSymbolsMessage(pkg) | |
| : '$pkg: treeshake is on, but record use is disabled, so the ' | |
| 'link hook cannot tell which functions the application ' | |
| 'uses. Build with record use enabled, or set `treeshake` ' | |
| 'to `auto`.', | |
| ); | |
| } | |
| // The pre-built dynamic library is equivalent, and doesn't need a C | |
| // toolchain. | |
| if (buildOptions.buildMode == NativeBuildMode.fetch && | |
| await _bundlePrebuiltDynamicLibrary(input, output, pkg, log)) { | |
| log.info( | |
| '$pkg: record use is disabled, bundled the pre-built dynamic ' | |
| 'library.', | |
| ); | |
| return; | |
| } | |
| if (input.config.code.targetOS == OS.windows) { | |
| throw BuildError( | |
| message: | |
| '$pkg: cannot link the static library for ${_target(input)} ' | |
| 'while record use is disabled, because a DLL only exports the ' | |
| 'functions it lists. Build with record use enabled, or set ' | |
| '`treeshake: off` under `hooks.user_defines.$pkg` to bundle a ' | |
| 'dynamic library instead.', | |
| ); | |
| } | |
| log.info('$pkg: record use is disabled, keeping all functions.'); | |
| symbols = null; | |
| } else { | |
| symbols = resolver(recordedUses); | |
| if (symbols.isEmpty) { | |
| log.info( | |
| '$pkg: the application uses none of the functions, so no library is ' | |
| 'bundled.', | |
| ); | |
| return; | |
| } | |
| log.info( | |
| '$pkg: keeping the ${symbols.length} functions the application ' | |
| 'uses:\n ${symbols.join('\n ')}', | |
| ); | |
| } | |
| try { | |
| await _linkStaticLibrary( | |
| input, | |
| output, | |
| pkg: pkg, | |
| staticLibrary: staticLibraryFile, | |
| symbols: symbols, | |
| log: log, | |
| ); | |
| } catch (e, s) { | |
| log.info('$pkg: linking failed: $e\n$s'); | |
| if (buildOptions.treeshake == TreeshakeMode.on) { | |
| rethrow; | |
| } | |
| // Tree-shaking only makes the library smaller, so in `auto` mode a | |
| // missing or broken C toolchain should not fail the build if there is an | |
| // equivalent pre-built dynamic library. This also catches Error (such as | |
| // ToolError). | |
| final fellBack = await _fallBackToPrebuiltLibrary( | |
| input, | |
| output, | |
| pkg: pkg, | |
| buildMode: buildOptions.buildMode, | |
| error: e, | |
| log: log, | |
| ); | |
| if (!fellBack) { | |
| rethrow; | |
| } | |
| } | |
| } | |
| /// Links [staticLibrary] into a dynamic library exporting [symbols], or all | |
| /// functions if `null` (not on Windows, where a DLL only exports the | |
| /// functions it lists). | |
| Future<void> _linkStaticLibrary( | |
| LinkInput input, | |
| LinkOutputBuilder output, { | |
| required String pkg, | |
| required Uri staticLibrary, | |
| required List<String>? symbols, | |
| required Logger log, | |
| }) async { | |
| final code = input.config.code; | |
| final linkerOptions = LinkerOptions.treeshake(symbolsToKeep: symbols); | |
| final linkLibraries = libraries?.call(code) ?? const <String>[]; | |
| final linkFrameworks = frameworks?.call(code); | |
| final linker = linkFrameworks == null | |
| // Omit `frameworks` to keep the default of `CLinker`. | |
| ? CLinker.library( | |
| name: name, | |
| packageName: pkg, | |
| assetName: assetName, | |
| sources: [staticLibrary.toFilePath()], | |
| libraries: linkLibraries, | |
| optimizationLevel: optimizationLevel, | |
| linkerOptions: linkerOptions, | |
| linkModePreference: LinkModePreference.dynamic, | |
| ) | |
| : CLinker.library( | |
| name: name, | |
| packageName: pkg, | |
| assetName: assetName, | |
| sources: [staticLibrary.toFilePath()], | |
| libraries: linkLibraries, | |
| frameworks: linkFrameworks, | |
| optimizationLevel: optimizationLevel, | |
| linkerOptions: linkerOptions, | |
| linkModePreference: LinkModePreference.dynamic, | |
| ); | |
| await linker.run(input: input, output: output, logger: log); | |
| } | |
| Future<bool> _bundlePrebuiltDynamicLibrary( | |
| LinkInput input, | |
| LinkOutputBuilder output, | |
| String pkg, | |
| Logger log, | |
| ) async { | |
| final config = releaseConfig; | |
| if (config == null) { | |
| return false; | |
| } | |
| final library = await _fetch(input, config, static: false, log: log); | |
| if (library == null) { | |
| return false; | |
| } | |
| output.assets.code.add( | |
| CodeAsset( | |
| package: pkg, | |
| name: assetName, | |
| linkMode: DynamicLoadingBundled(), | |
| file: library, | |
| ), | |
| ); | |
| return true; | |
| } | |
| Future<bool> _fallBackToPrebuiltLibrary( | |
| LinkInput input, | |
| LinkOutputBuilder output, { | |
| required String pkg, | |
| required NativeBuildMode buildMode, | |
| required Object error, | |
| required Logger log, | |
| }) async { | |
| final target = _target(input); | |
| if (buildMode != NativeBuildMode.fetch) { | |
| log.severe( | |
| 'package:$pkg could not link the static library built in the ' | |
| '`${buildMode.name}` build mode for $target. Install a C toolchain ' | |
| '(compiler and linker) for $target. Only the `fetch` build mode falls ' | |
| 'back to the pre-built dynamic library, which could differ from the ' | |
| 'library built in other modes.', | |
| ); | |
| return false; | |
| } | |
| final reason = switch (error) { | |
| ProcessException(:final executable) => | |
| 'ProcessException: $executable failed', | |
| _ => error.toString().split('\n').first, | |
| }; | |
| if (!await _bundlePrebuiltDynamicLibrary(input, output, pkg, log)) { | |
| return false; | |
| } | |
| log.warning( | |
| 'Warning: package:$pkg could not tree-shake its native library for ' | |
| '$target, so it bundles the pre-built dynamic library of the $pkg ' | |
| '${releaseConfig!.version} release instead, which is not tree-shaken ' | |
| 'and therefore larger. To enable tree-shaking, install a C toolchain ' | |
| '(compiler and linker) for $target. Linking failed with: $reason', | |
| ); | |
| return true; | |
| } | |
| static String _target(HookInput input) { | |
| final code = input.config.code; | |
| return '${code.targetOS}_${code.targetArchitecture}'; | |
| } | |
| static String _noUsedSymbolsMessage(String pkg) => | |
| '$pkg: treeshake is on, but this package does not support ' | |
| 'tree-shaking, because its `PrebuiltLibrary` has no `usedSymbols`.'; | |
| /// Synthesizes a [BuildInput] via [BuildInputBuilder] (following the | |
| /// `download_asset/tool/build.dart` pattern in `package:hooks`) and invokes | |
| /// [buildFromSource] for standalone precompilation in CI scripts. | |
| /// | |
| /// [packageName] defaults to [PrebuiltLibrary.packageName], then to [name]. | |
| Future<Uri> buildStandalone({ | |
| required OS targetOS, | |
| required Architecture targetArchitecture, | |
| required bool static, | |
| IOSSdk? iOSSdk, | |
| String? packageName, | |
| Uri? packageRoot, | |
| Uri? outputDirectory, | |
| Uri? outputDirectoryShared, | |
| Uri? checkoutPath, | |
| int androidTargetNdkApi = 28, | |
| int iOSTargetVersion = 13, | |
| int macOSTargetVersion = 13, | |
| }) async { | |
| final callback = buildFromSource; | |
| if (callback == null) { | |
| throw StateError( | |
| 'Cannot call buildStandalone when `buildFromSource` is null.', | |
| ); | |
| } | |
| final root = packageRoot ?? Directory.current.uri; | |
| final pkg = packageName ?? this.packageName ?? name; | |
| final outDir = | |
| outputDirectory ?? | |
| root.resolve( | |
| '.dart_tool/prebuilt_code_assets/' | |
| '${targetOS.name}_${targetArchitecture.name}_' | |
| '${static ? "static" : "dynamic"}/', | |
| ); | |
| final sharedDir = | |
| outputDirectoryShared ?? | |
| root.resolve('.dart_tool/prebuilt_code_assets/shared/'); | |
| final outFile = outDir.resolve('output.json'); | |
| await Directory.fromUri(outDir).create(recursive: true); | |
| await Directory.fromUri(sharedDir).create(recursive: true); | |
| final inputBuilder = BuildInputBuilder() | |
| ..setupShared( | |
| packageRoot: root, | |
| packageName: pkg, | |
| outputFile: outFile, | |
| outputDirectoryShared: sharedDir, | |
| ) | |
| ..config.setupBuild(linkingEnabled: static) | |
| ..addExtension( | |
| CodeAssetExtension( | |
| targetArchitecture: targetArchitecture, | |
| targetOS: targetOS, | |
| linkModePreference: static | |
| ? LinkModePreference.static | |
| : LinkModePreference.dynamic, | |
| android: targetOS == OS.android | |
| ? AndroidCodeConfig(targetNdkApi: androidTargetNdkApi) | |
| : null, | |
| iOS: targetOS == OS.iOS | |
| ? IOSCodeConfig( | |
| targetSdk: iOSSdk ?? IOSSdk.iPhoneOS, | |
| targetVersion: iOSTargetVersion, | |
| ) | |
| : null, | |
| macOS: targetOS == OS.macOS | |
| ? MacOSCodeConfig(targetVersion: macOSTargetVersion) | |
| : null, | |
| ), | |
| ); | |
| return callback( | |
| inputBuilder.build(), | |
| BuildOutputBuilder(), | |
| static: static, | |
| checkoutPath: checkoutPath, | |
| ); | |
| } | |
| } |