linkSlintAot function

Future<void> linkSlintAot(
  1. LinkInput input,
  2. LinkOutputBuilder output
)

App link hook: links the final slint-dart-aot dylib from the staticlib the build hook (aot_build.dart) routed here, keeping only the components the app's Dart code actually uses.

Liveness comes from @RecordUse() on each component's generated _new extern: the AOT compiler records tear-offs that survive Dart tree-shaking, and this hook drops the native code of every component without one. When the toolchain provides no recordings (LinkInput.recordedUses is null), every component is kept — the link then matches what a plain cdylib build would have produced.

Wire it up as the app's hook/link.dart:

import 'package:hooks/hooks.dart';
import 'package:slint_compiler/aot_link.dart';

void main(List<String> args) => link(args, linkSlintAot);

Implementation

Future<void> linkSlintAot(LinkInput input, LinkOutputBuilder output) async {
  final staticLibs = [
    for (final a in input.assets.code)
      if (a.linkMode is StaticLinking && a.file != null) a,
  ];
  if (staticLibs.isEmpty) return; // nothing routed (e.g. debug build)
  if (staticLibs.length > 1) {
    throw StateError(
      'expected one routed slint staticlib, got '
      '${[for (final a in staticLibs) a.id]}',
    );
  }
  final staticLib = staticLibs.single;

  final manifest = jsonDecode(
    File.fromUri(staticLib.file!.resolve(aotLinkManifestName))
        .readAsStringSync(),
  ) as Map<String, Object?>;
  final components =
      (manifest['components'] as List).cast<Map<String, Object?>>();
  final assetIds = (manifest['assetIds'] as List).cast<String>();
  final sharedSymbols = (manifest['sharedSymbols'] as List).cast<String>();
  final linkFlags = (manifest['linkFlags'] as List).cast<String>();

  final used = usedComponentNames(input.recordedUses, components);
  final dropped = [
    for (final c in components)
      if (!used.contains(c['name'])) c['name'] as String,
  ];
  stderr.writeln(
    'slint_aot link: keeping ${used.length}/${components.length} components'
    '${dropped.isEmpty ? '' : ', dropping $dropped'}'
    '${input.recordedUses == null ? ' (no recorded usages from toolchain)' : ''}',
  );

  final keepSymbols = [
    ...sharedSymbols,
    for (final c in components)
      if (used.contains(c['name']))
        ...(c['symbols'] as List).cast<String>(),
  ];
  final flags = partitionLinkFlags(linkFlags);
  final os = input.config.code.targetOS;

  await CLinker.library(
    name: aotLibraryName,
    sources: [staticLib.file!.toFilePath()],
    // RunCBuilder only emits `-framework` flags for Language.objectiveC
    // (documented on CTool.frameworks); it changes nothing else about a
    // pure link step, and without it the rustc-reported frameworks are
    // silently dropped — undefined CoreText/CoreFoundation symbols.
    language: Language.objectiveC,
    frameworks: flags.frameworks,
    libraries: flags.libraries,
    flags: flags.other,
    linkerOptions: LinkerOptions.treeshake(
      symbolsToKeep: keepSymbols,
      // ld's -x drops local symbols — the moral equivalent of cargo's
      // strip = "symbols" on the old cdylib path (2 MB per arch on the
      // example). Not a flag MSVC's LINK.exe knows.
      flags: [if (os != OS.windows) '-x'],
    ),
  ).run(input: input, output: output);

  // CLinker linked one dylib; every .slint file's asset id points at it.
  final libUri = input.outputDirectory
      .resolve(os.libraryFileName(aotLibraryName, DynamicLoadingBundled()));
  for (final id in assetIds) {
    output.assets.code.add(
      CodeAsset(
        package: input.packageName,
        name: id,
        linkMode: DynamicLoadingBundled(),
        file: libUri,
      ),
    );
  }
  output.dependencies.add(staticLib.file!.resolve(aotLinkManifestName));
}