Files
clide/lib/kernel/src/toolchain.dart
T
jpmschweitzerandClaude 5ee759d0fd clear dart doc validate-links warnings
Eight unresolved doc references and broken README-rewritten links
that surfaced under `dart doc --validate-links`:

- Library-scope refs `[spawn]`, `[openProject]` qualified to
  `[Backend.spawn]` / `[Backend.openProject]`; same treatment for
  `[resolvePaths]` / `[applyResolved]` on Toolchain.
- `[D-41]` was a decision ID, not a Dart symbol — drop the brackets.
- `[from]` from I18n.interpolated qualified to `[I18nReplacer.from]`.
- `[DefaultSurfaceMap]` was a stale name (private `_defaultSurfaceMap`
  in resolver.dart); switch to backticked path reference since
  dartdoc can't link private members.
- `[icons/]` was a directory, not a symbol; backticked path.
- README links to `legacy/`, `docs/initial-plan.md`, `decisions/`,
  `LICENSE` rewritten as absolute github.com/postmeridiem/clide URLs
  so dartdoc stops re-rooting them into the doc tree.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-05-06 17:58:18 +02:00

229 lines
6.4 KiB
Dart

/// Centralized binary resolution for external tools.
///
/// Resolution runs in a background isolate via [Toolchain.resolvePaths]
/// to avoid blocking the merged UI/platform thread on macOS. The result
/// is applied on the main thread via [Toolchain.applyResolved].
library;
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
/// Serializable result of tool resolution (crosses isolate boundary).
class ResolvedPaths {
const ResolvedPaths({
this.git,
this.pql,
this.tmux,
this.ptyc,
this.shell,
this.gitEnv,
});
final String? git;
final String? pql;
final String? tmux;
final String? ptyc;
final String? shell;
final Map<String, String>? gitEnv;
}
class Toolchain extends ChangeNotifier {
String? _git;
String? _pql;
String? _tmux;
String? _ptyc;
String? _shell;
Map<String, String>? _gitEnv;
bool _resolved = false;
String get git => _git ?? 'git';
String get pql => _pql ?? 'pql';
String get tmux => _tmux ?? 'tmux';
String get ptyc => _ptyc ?? 'ptyc';
String get shell => _shell ?? '/bin/bash';
/// Extra environment variables for git (e.g. GIT_EXEC_PATH for dugite).
Map<String, String>? get gitEnv => _gitEnv;
bool get resolved => _resolved;
bool get allOk => _resolved && missing.isEmpty;
List<String> get missing => [
if (_git == null) 'git',
if (_pql == null) 'pql',
if (_tmux == null) 'tmux',
];
/// Returns a Future that completes when resolution finishes.
Future<void> waitForResolution() {
if (_resolved) return Future.value();
final c = Completer<void>();
void listener() {
if (_resolved) {
removeListener(listener);
if (!c.isCompleted) c.complete();
}
}
addListener(listener);
return c.future;
}
/// Apply paths resolved in a background isolate.
void applyResolved(ResolvedPaths p) {
_git = p.git;
_pql = p.pql;
_tmux = p.tmux;
_ptyc = p.ptyc;
_shell = p.shell;
_gitEnv = p.gitEnv;
_resolved = true;
notifyListeners();
}
/// Pure function — runs in a background isolate. All file I/O happens
/// here, off the main thread.
static ResolvedPaths resolvePaths({required String workspaceRoot}) {
final dugite = '$workspaceRoot/native/dugite/bin';
String? git;
Map<String, String>? gitEnv;
final dugiteGit = _firstExisting(['$dugite/git']);
if (dugiteGit != null) {
git = dugiteGit;
final dugiteRoot = File(dugiteGit).parent.parent.path;
gitEnv = {
'GIT_EXEC_PATH': '$dugiteRoot/libexec/git-core',
'GIT_TEMPLATE_DIR': '$dugiteRoot/share/git-core/templates',
};
} else {
git = _findOnPath('git');
}
final pql = _findOnPath('pql');
final tmux = _findOnPath('tmux');
final shell = _findOnPath(Platform.environment['SHELL']?.split('/').last ?? 'bash');
final ptyc = _firstExisting([
'$workspaceRoot/ptyc/bin/ptyc',
'$workspaceRoot/native/linux-x64/ptyc',
'$workspaceRoot/native/macos-arm64/ptyc',
'$workspaceRoot/native/macos-x64/ptyc',
if (Platform.environment['HOME'] case final home?) '$home/.local/bin/ptyc',
]) ??
_findOnPath('ptyc');
return ResolvedPaths(
git: git,
pql: pql,
tmux: tmux,
ptyc: ptyc,
shell: shell,
gitEnv: gitEnv,
);
}
static String? _findOnPath(String name) {
for (final dir in _expandedPath().split(':')) {
if (dir.isEmpty) continue;
final f = File('$dir/$name');
if (f.existsSync()) return f.path;
}
return null;
}
/// Build expanded PATH inline — must be self-contained for isolate use.
static String _expandedPath() {
final base = Platform.environment['PATH'] ?? '';
if (!Platform.isMacOS) return base;
final home = Platform.environment['HOME'] ?? '';
final extras = <String>[
if (home.isNotEmpty) '$home/.local/bin',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/bin',
];
final existing = base.split(':').toSet();
final missing = extras.where((p) => !existing.contains(p));
if (missing.isEmpty) return base;
return [...missing, ...existing].join(':');
}
static String? _firstExisting(List<String> candidates) {
for (final c in candidates) {
if (File(c).existsSync()) return c;
}
return null;
}
}
/// Top-level function for compute/isolate use. Takes a single String
/// argument (the workspace root) and returns a plain-data result.
ResolvedPaths resolveToolchainPaths(String workspaceRoot) {
final dugite = '$workspaceRoot/native/dugite/bin';
String? git;
Map<String, String>? gitEnv;
final dugiteGit = _firstExistingStandalone(['$dugite/git']);
if (dugiteGit != null) {
git = dugiteGit;
final dugiteRoot = File(dugiteGit).parent.parent.path;
gitEnv = {
'GIT_EXEC_PATH': '$dugiteRoot/libexec/git-core',
'GIT_TEMPLATE_DIR': '$dugiteRoot/share/git-core/templates',
};
} else {
git = _findOnPathStandalone('git');
}
return ResolvedPaths(
git: git,
pql: _findOnPathStandalone('pql'),
tmux: _findOnPathStandalone('tmux'),
ptyc: _firstExistingStandalone([
'$workspaceRoot/ptyc/bin/ptyc',
'$workspaceRoot/native/linux-x64/ptyc',
'$workspaceRoot/native/macos-arm64/ptyc',
'$workspaceRoot/native/macos-x64/ptyc',
if (Platform.environment['HOME'] case final home?) '$home/.local/bin/ptyc',
]) ??
_findOnPathStandalone('ptyc'),
shell: _findOnPathStandalone(Platform.environment['SHELL']?.split('/').last ?? 'bash'),
gitEnv: gitEnv,
);
}
String? _findOnPathStandalone(String name) {
for (final dir in _expandedPathStandalone().split(':')) {
if (dir.isEmpty) continue;
final f = File('$dir/$name');
if (f.existsSync()) return f.path;
}
return null;
}
String? _firstExistingStandalone(List<String> candidates) {
for (final c in candidates) {
if (File(c).existsSync()) return c;
}
return null;
}
String _expandedPathStandalone() {
final base = Platform.environment['PATH'] ?? '';
if (!Platform.isMacOS) return base;
final home = Platform.environment['HOME'] ?? '';
final extras = <String>[
if (home.isNotEmpty) '$home/.local/bin',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/bin',
];
final existing = base.split(':').toSet();
final missing = extras.where((p) => !existing.contains(p));
if (missing.isEmpty) return base;
return [...missing, ...existing].join(':');
}