fix(cpp-front): discover libclang on Linux, not only on Homebrew macOS #8
2 changed files with 93 additions and 12 deletions
|
|
@ -8,12 +8,48 @@ import 'package:test/test.dart';
|
|||
/// state model, and the unchanged rules must fire on each positive control in
|
||||
/// test/fixtures/cpp_state and stay silent on each negative one.
|
||||
///
|
||||
/// Skipped, not passed, when Homebrew LLVM (libclang + its Python bindings) or
|
||||
/// python3 is unavailable.
|
||||
/// Skipped, not passed, when libclang (plus its Python bindings) or python3 is
|
||||
/// unavailable.
|
||||
///
|
||||
/// Discovery mirrors tool/cpp_front.py deliberately. This test used to look
|
||||
/// only for Homebrew's `libclang.dylib`, so on Linux it skipped rather than
|
||||
/// ran -- which is how the front end came to be unusable there without any
|
||||
/// suite noticing.
|
||||
String _llvmPrefix() {
|
||||
final env = Platform.environment['PIPE_LLVM'];
|
||||
if (env != null && env.isNotEmpty) return env;
|
||||
return Platform.isMacOS ? '/opt/homebrew/opt/llvm' : '/usr';
|
||||
}
|
||||
|
||||
/// The library, by platform and possibly versioned: Linux distributions ship
|
||||
/// `libclang.so.<major>.<minor>` and do not always add the unversioned
|
||||
/// symlink, and Debian/Ubuntu keep it under a multiarch subdirectory.
|
||||
bool _libclangPresent(String prefix) {
|
||||
final suffix = Platform.isMacOS ? 'dylib' : 'so';
|
||||
final roots = <Directory>[Directory('$prefix/lib')];
|
||||
final libDir = Directory('$prefix/lib');
|
||||
if (libDir.existsSync()) {
|
||||
roots.addAll(
|
||||
libDir.listSync().whereType<Directory>().where(
|
||||
(d) => d.path.endsWith('-linux-gnu'),
|
||||
),
|
||||
);
|
||||
}
|
||||
for (final root in roots) {
|
||||
if (!root.existsSync()) continue;
|
||||
if (File('${root.path}/libclang.$suffix').existsSync()) return true;
|
||||
final versioned = root.listSync().whereType<File>().any(
|
||||
(f) => f.uri.pathSegments.last.startsWith('libclang.$suffix'),
|
||||
);
|
||||
if (versioned) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void main() {
|
||||
final llvm = Platform.environment['PIPE_LLVM'] ?? '/opt/homebrew/opt/llvm';
|
||||
final llvm = _llvmPrefix();
|
||||
final available =
|
||||
File('$llvm/lib/libclang.dylib').existsSync() &&
|
||||
_libclangPresent(llvm) &&
|
||||
Process.runSync('python3', ['--version']).exitCode == 0;
|
||||
|
||||
late FindingReport report;
|
||||
|
|
@ -60,7 +96,7 @@ void main() {
|
|||
hits(ruleEmittedNotRendered).map((f) => f.data['union']),
|
||||
contains('fx::Unread'),
|
||||
);
|
||||
}, skip: available ? false : 'Homebrew LLVM / python3 not available');
|
||||
}, skip: available ? false : 'libclang / python3 not available');
|
||||
|
||||
test('an enumerator examined but never produced is a dead branch', () {
|
||||
final ghost = hits(
|
||||
|
|
@ -69,7 +105,7 @@ void main() {
|
|||
expect(ghost, hasLength(1));
|
||||
expect(ghost.single.file, 'src/engine.cpp');
|
||||
expect(ghost.single.line, isNotNull);
|
||||
}, skip: available ? false : 'Homebrew LLVM / python3 not available');
|
||||
}, skip: available ? false : 'libclang / python3 not available');
|
||||
|
||||
test('a cast to one enum does not mute "never emitted" for another', () {
|
||||
// Casted::Y is produced only through static_cast, so it is unprovable
|
||||
|
|
@ -79,7 +115,7 @@ void main() {
|
|||
isNot(contains('fx::Casted')),
|
||||
);
|
||||
expect(report.suppressed.where((s) => s.contains('fx::Phase')), isEmpty);
|
||||
}, skip: available ? false : 'Homebrew LLVM / python3 not available');
|
||||
}, skip: available ? false : 'libclang / python3 not available');
|
||||
|
||||
test(
|
||||
'a swallowed failure on the method\'s own busy state fires, and only it',
|
||||
|
|
@ -90,6 +126,6 @@ void main() {
|
|||
'startSwallowed',
|
||||
]);
|
||||
},
|
||||
skip: available ? false : 'Homebrew LLVM / python3 not available',
|
||||
skip: available ? false : 'libclang / python3 not available',
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ Consequences worth knowing before reading the findings:
|
|||
is recorded as an indirect emission so the rule suppresses itself.
|
||||
- The rule messages still speak of "UI"; for C++ read "nothing observes it".
|
||||
|
||||
Requires Homebrew LLVM's libclang and its Python bindings (found automatically).
|
||||
Requires libclang and its Python bindings (found automatically): Homebrew's
|
||||
LLVM on macOS, the system LLVM elsewhere. Set PIPE_LLVM to point at another
|
||||
prefix.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -41,13 +43,46 @@ import json
|
|||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from pathlib import Path
|
||||
|
||||
LLVM = Path(os.environ.get("PIPE_LLVM", "/opt/homebrew/opt/llvm"))
|
||||
def _llvm_prefix() -> Path:
|
||||
"""Where libclang lives. PIPE_LLVM wins; otherwise Homebrew's LLVM on macOS
|
||||
and the system prefix elsewhere, because Linux has no /opt/homebrew."""
|
||||
env = os.environ.get("PIPE_LLVM")
|
||||
if env:
|
||||
return Path(env)
|
||||
return Path("/opt/homebrew/opt/llvm" if sys.platform == "darwin" else "/usr")
|
||||
|
||||
|
||||
LLVM = _llvm_prefix()
|
||||
|
||||
|
||||
def _libclang_path() -> str:
|
||||
"""The shared library, by platform and possibly versioned.
|
||||
|
||||
Linux distributions name it libclang.so.<major>.<minor> and do not always
|
||||
ship the unversioned symlink, and Debian/Ubuntu put it under a multiarch
|
||||
subdirectory -- so a single fixed name finds nothing on most of them.
|
||||
"""
|
||||
suffix = "dylib" if sys.platform == "darwin" else "so"
|
||||
roots = [LLVM / "lib", *(Path(p) for p in glob.glob(str(LLVM / "lib/*-linux-gnu")))]
|
||||
for root in roots:
|
||||
exact = root / f"libclang.{suffix}"
|
||||
if exact.exists():
|
||||
return str(exact)
|
||||
for root in roots:
|
||||
# Highest version first: libclang.so.22.1 before libclang.so.21.1.
|
||||
versioned = sorted(glob.glob(str(root / f"libclang.{suffix}*")), reverse=True)
|
||||
if versioned:
|
||||
return versioned[0]
|
||||
# Nothing found: hand clang.cindex the conventional name so its own error
|
||||
# names the library rather than a path this function invented.
|
||||
return str(LLVM / f"lib/libclang.{suffix}")
|
||||
|
||||
|
||||
def _load_cindex():
|
||||
|
|
@ -58,7 +93,7 @@ def _load_cindex():
|
|||
|
||||
# Pool workers are reused, and libclang may be configured only once.
|
||||
if not cindex.Config.loaded:
|
||||
cindex.Config.set_library_file(str(LLVM / "lib/libclang.dylib"))
|
||||
cindex.Config.set_library_file(_libclang_path())
|
||||
return cindex
|
||||
|
||||
|
||||
|
|
@ -81,8 +116,18 @@ _TRANSPARENT = {
|
|||
}
|
||||
|
||||
|
||||
def _clang_binary() -> str:
|
||||
"""The driver used only for -print-resource-dir. Under a system prefix
|
||||
LLVM/bin/clang exists, but a prefix that ships just the library does not
|
||||
have one, so fall back to whatever is on PATH."""
|
||||
candidate = LLVM / "bin/clang"
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
return shutil.which("clang") or shutil.which("clang++") or str(candidate)
|
||||
|
||||
|
||||
def _resource_dir() -> str:
|
||||
return subprocess.check_output([str(LLVM / "bin/clang"), "-print-resource-dir"], text=True).strip()
|
||||
return subprocess.check_output([_clang_binary(), "-print-resource-dir"], text=True).strip()
|
||||
|
||||
|
||||
def _sdk_root() -> str | None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue