v31.1-roots.1 · Commit 16: [PATCH 16/17] fix(build): classify Homebrew libevent headers as system
ci/test/test_homebrew_libevent_headers.py
Before: /dev/null · After: ci/test/test_homebrew_libevent_headers.py
File metadata
new file mode 100644 index 0000000000000000000000000000000000000000..4c9688d2eba63ed1699e1f0076904989dfe73161
Download original patch · Release notes
Diff rows 1–177 of 177. Line numbers refer to the original before/after files.
+ Added− RemovedUnmarked lines are context.
Scroll the code area horizontally to read long lines. Before is on the left; after is on the right. Unified view fits narrow screens more easily.
ci/test/test_homebrew_libevent_headers.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
# Copyright (c) 2026-present The Bitcoin Roots developers
|
| 3 |
+
# Distributed under the MIT software license, see the accompanying
|
| 4 |
+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
| 5 |
+
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import json
|
| 8 |
+
import os
|
| 9 |
+
import shlex
|
| 10 |
+
import shutil
|
| 11 |
+
import subprocess
|
| 12 |
+
import tempfile
|
| 13 |
+
import unittest
|
| 14 |
+
|
| 15 |
+
ROOT = Path(__file__).resolve().parents[2]
|
| 16 |
+
CLANG = shutil.which('clang++')
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@unittest.skipUnless(CLANG and shutil.which('cmake') and shutil.which('pkg-config'),
|
| 20 |
+
'CMake, Clang and pkg-config are required')
|
| 21 |
+
class HomebrewLibeventHeadersTest(unittest.TestCase):
|
| 22 |
+
def build(self, discovery, *, apple=True, mismatch=False, implicit=True,
|
| 23 |
+
project_warning=False, brew_success=True, alias=True, extra_mismatch=False):
|
| 24 |
+
with tempfile.TemporaryDirectory(prefix='roots-homebrew-libevent-') as directory:
|
| 25 |
+
work = Path(directory)
|
| 26 |
+
cellar = work / 'Cellar/libevent/2.1.13'
|
| 27 |
+
prefix = work / 'homebrew prefix'
|
| 28 |
+
headers = cellar / 'include/event2'
|
| 29 |
+
headers.mkdir(parents=True)
|
| 30 |
+
# Reproduce the native libevent documentation diagnostic, without
|
| 31 |
+
# changing or copying third-party source into the repository.
|
| 32 |
+
(headers / 'event.h').write_text(
|
| 33 |
+
'/** @param missing Not a parameter. */\n'
|
| 34 |
+
'void event_probe(int actual);\n')
|
| 35 |
+
(headers / 'http.h').write_text(
|
| 36 |
+
'#include <cstdint>\nstruct evhttp_connection {};\n'
|
| 37 |
+
'inline void evhttp_connection_get_peer(evhttp_connection*, '
|
| 38 |
+
'const char**, uint16_t*) {}\n')
|
| 39 |
+
(prefix / 'include').mkdir(parents=True)
|
| 40 |
+
if alias:
|
| 41 |
+
if mismatch:
|
| 42 |
+
shutil.copytree(headers, prefix / 'include/event2')
|
| 43 |
+
else:
|
| 44 |
+
(prefix / 'include/event2').symlink_to(headers, target_is_directory=True)
|
| 45 |
+
other = work / 'other-libevent'
|
| 46 |
+
if extra_mismatch:
|
| 47 |
+
shutil.copytree(headers, other / 'include/event2')
|
| 48 |
+
tools = work / 'tools'
|
| 49 |
+
tools.mkdir()
|
| 50 |
+
brew = tools / 'brew'
|
| 51 |
+
brew.write_text('#!/bin/sh\n' + (
|
| 52 |
+
'case "$*" in\n'
|
| 53 |
+
f' "--prefix") printf "%s\\n" {shlex.quote(str(prefix))} ;;\n'
|
| 54 |
+
' *) exit 1 ;;\nesac\n' if brew_success else 'exit 1\n'))
|
| 55 |
+
brew.chmod(0o755)
|
| 56 |
+
config = work / 'config'
|
| 57 |
+
config.mkdir()
|
| 58 |
+
pc = work / 'pkgconfig'
|
| 59 |
+
pc.mkdir()
|
| 60 |
+
configs = ['set(Libevent_VERSION "2.1.13")\n']
|
| 61 |
+
for component in ('core', 'extra', 'pthreads'):
|
| 62 |
+
selected = other if extra_mismatch and component == 'extra' else cellar
|
| 63 |
+
(selected / 'lib').mkdir(exist_ok=True)
|
| 64 |
+
configs.append(
|
| 65 |
+
f'if(NOT TARGET libevent::{component})\n'
|
| 66 |
+
f' add_library(libevent::{component} INTERFACE IMPORTED)\n'
|
| 67 |
+
f' set_target_properties(libevent::{component} PROPERTIES\n'
|
| 68 |
+
f' INTERFACE_INCLUDE_DIRECTORIES "{selected / "include"}")\n'
|
| 69 |
+
'endif()\n')
|
| 70 |
+
(pc / f'libevent_{component}.pc').write_text(
|
| 71 |
+
f'prefix={selected}\nlibdir=${{prefix}}/lib\nincludedir=${{prefix}}/include\n'
|
| 72 |
+
f'Name: libevent_{component}\nDescription: Test fixture\nVersion: 2.1.13\n'
|
| 73 |
+
'Libs: -L${libdir}\nCflags: -I${includedir}\n')
|
| 74 |
+
(config / 'LibeventConfig.cmake').write_text(''.join(configs))
|
| 75 |
+
(config / 'LibeventConfigVersion.cmake').write_text(
|
| 76 |
+
'set(PACKAGE_VERSION "2.1.13")\nset(PACKAGE_VERSION_COMPATIBLE TRUE)\n')
|
| 77 |
+
source = work / 'source'
|
| 78 |
+
source.mkdir()
|
| 79 |
+
(source / 'probe.cpp').write_text(
|
| 80 |
+
'#include <event2/event.h>\n' + (
|
| 81 |
+
'/** @param wrong Not a parameter. */\nvoid project_probe(int actual);\n'
|
| 82 |
+
if project_warning else ''))
|
| 83 |
+
implicit_include = (
|
| 84 |
+
f'list(APPEND CMAKE_CXX_IMPLICIT_INCLUDE_DIRECTORIES "{prefix / "include"}")\n'
|
| 85 |
+
if implicit else '')
|
| 86 |
+
(source / 'CMakeLists.txt').write_text(
|
| 87 |
+
'cmake_minimum_required(VERSION 3.22)\n'
|
| 88 |
+
'project(HomebrewLibeventHeaders LANGUAGES CXX)\n'
|
| 89 |
+
f'set(CMAKE_HOST_APPLE {"TRUE" if apple else "FALSE"})\n'
|
| 90 |
+
f'{implicit_include}'
|
| 91 |
+
f'list(PREPEND CMAKE_MODULE_PATH "{ROOT / "cmake/module"}")\n'
|
| 92 |
+
'find_package(Libevent 2.1.8 MODULE REQUIRED)\n'
|
| 93 |
+
'foreach(component core extra pthreads)\n'
|
| 94 |
+
' add_library(probe_${component} OBJECT probe.cpp)\n'
|
| 95 |
+
' target_link_libraries(probe_${component} PRIVATE libevent::${component})\n'
|
| 96 |
+
' target_compile_options(probe_${component} PRIVATE -Wdocumentation -Werror)\n'
|
| 97 |
+
'endforeach()\n')
|
| 98 |
+
environment = {**os.environ, 'PATH': f'{tools}:{os.environ["PATH"]}',
|
| 99 |
+
'PKG_CONFIG_LIBDIR': str(pc), 'PKG_CONFIG_PATH': ''}
|
| 100 |
+
environment.pop('CPATH', None)
|
| 101 |
+
build = work / 'build'
|
| 102 |
+
configure = subprocess.run(
|
| 103 |
+
['cmake', '-S', str(source), '-B', str(build),
|
| 104 |
+
f'-DCMAKE_CXX_COMPILER={CLANG}', '-DCMAKE_EXPORT_COMPILE_COMMANDS=ON',
|
| 105 |
+
f'-DLibevent_DIR={config if discovery == "config" else work / "absent"}'],
|
| 106 |
+
capture_output=True, text=True, env=environment)
|
| 107 |
+
self.assertEqual(configure.returncode, 0, configure.stdout + configure.stderr)
|
| 108 |
+
if discovery == 'pkgconfig':
|
| 109 |
+
self.assertIn('Found PkgConfig:', configure.stdout)
|
| 110 |
+
else:
|
| 111 |
+
self.assertIn(f'Found Libevent: {config}', configure.stdout)
|
| 112 |
+
commands = json.loads((build / 'compile_commands.json').read_text())
|
| 113 |
+
self.assertEqual(len(commands), 3)
|
| 114 |
+
result = subprocess.run(
|
| 115 |
+
['cmake', '--build', str(build), '--parallel', '1'],
|
| 116 |
+
capture_output=True, text=True,
|
| 117 |
+
env={**environment, 'CPATH': str(prefix / 'include')})
|
| 118 |
+
for entry in commands:
|
| 119 |
+
argv = shlex.split(entry['command'])
|
| 120 |
+
system_dirs = [argv[i + 1] for i, arg in enumerate(argv[:-1]) if arg == '-isystem']
|
| 121 |
+
expected = apple and not mismatch and brew_success and alias
|
| 122 |
+
if extra_mismatch and 'probe_extra.dir' in entry['command']:
|
| 123 |
+
expected = False
|
| 124 |
+
self.assertEqual(str(prefix / 'include') in system_dirs, expected,
|
| 125 |
+
entry['command'] + result.stderr)
|
| 126 |
+
self.assertIn('-Werror', argv)
|
| 127 |
+
self.assertIn('-Wdocumentation', argv)
|
| 128 |
+
return result
|
| 129 |
+
|
| 130 |
+
def both(self, **kwargs):
|
| 131 |
+
results = []
|
| 132 |
+
for discovery in ('config', 'pkgconfig'):
|
| 133 |
+
with self.subTest(discovery=discovery):
|
| 134 |
+
results.append(self.build(discovery, **kwargs))
|
| 135 |
+
return results
|
| 136 |
+
|
| 137 |
+
def test_implicit_matching_alias_emits_flag_and_compiles(self):
|
| 138 |
+
for result in self.both():
|
| 139 |
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
| 140 |
+
|
| 141 |
+
def test_explicit_matching_alias_compiles(self):
|
| 142 |
+
for result in self.both(implicit=False):
|
| 143 |
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
| 144 |
+
|
| 145 |
+
def test_project_documentation_warnings_still_fail(self):
|
| 146 |
+
for result in self.both(project_warning=True):
|
| 147 |
+
self.assertNotEqual(result.returncode, 0)
|
| 148 |
+
self.assertIn('source/probe.cpp:', result.stderr)
|
| 149 |
+
self.assertIn('[-Werror,-Wdocumentation]', result.stderr)
|
| 150 |
+
|
| 151 |
+
def test_mismatched_installation_is_not_hidden(self):
|
| 152 |
+
for result in self.both(mismatch=True):
|
| 153 |
+
self.assertNotEqual(result.returncode, 0)
|
| 154 |
+
self.assertIn('homebrew prefix/include/event2/event.h:', result.stderr)
|
| 155 |
+
|
| 156 |
+
def test_mismatched_component_is_not_hidden(self):
|
| 157 |
+
for result in self.both(extra_mismatch=True):
|
| 158 |
+
self.assertNotEqual(result.returncode, 0)
|
| 159 |
+
self.assertIn('homebrew prefix/include/event2/event.h:', result.stderr)
|
| 160 |
+
|
| 161 |
+
def test_non_apple_host_is_unchanged(self):
|
| 162 |
+
for result in self.both(apple=False):
|
| 163 |
+
self.assertNotEqual(result.returncode, 0)
|
| 164 |
+
self.assertIn('[-Werror,-Wdocumentation]', result.stderr)
|
| 165 |
+
|
| 166 |
+
def test_failed_brew_query_is_unchanged(self):
|
| 167 |
+
for result in self.both(brew_success=False):
|
| 168 |
+
self.assertNotEqual(result.returncode, 0)
|
| 169 |
+
self.assertIn('[-Werror,-Wdocumentation]', result.stderr)
|
| 170 |
+
|
| 171 |
+
def test_missing_alias_uses_selected_system_headers(self):
|
| 172 |
+
for result in self.both(alias=False):
|
| 173 |
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
if __name__ == '__main__':
|
| 177 |
+
unittest.main()
|