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. The two line-number columns refer to before and after.
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()
|