Skip to content

Commit 2ed5091

Browse files
committed
FEATURE-ci-v3-integration: repair platform configure and link boundaries
1 parent 919fa6a commit 2ed5091

5 files changed

Lines changed: 119 additions & 14 deletions

File tree

‎scripts/check-v3-installed-consumer.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,17 @@ def package_lane(args, source, output, mode, inventory):
4545
env['V3_TLS_CFLAGS'] = '-I' + (args.provider_prefix / 'include').as_posix()
4646
env['V3_TLS_LIBS'] = '-L' + (args.provider_prefix / 'lib').as_posix() + ' -lssl -lcrypto'
4747
bash = 'bash'
48+
configure = (source / 'configure').as_posix()
4849
if platform.system() == 'Windows':
4950
# CreateProcess searches System32 before PATH for an unqualified name.
5051
bash = shutil.which('bash', path=env.get('PATH', ''))
5152
if not bash:
5253
raise ValueError('bash is unavailable on the selected PATH')
5354
bash = str(Path(bash).resolve())
54-
command([bash, (source / 'configure').as_posix(), '--enable-v3-tls=' + mode, '--disable-examples',
55+
# Autoconf splits auxiliary-file search directories on ':', so its
56+
# source path must use MSYS notation rather than a Windows drive name.
57+
configure = command([bash, '-c', 'cygpath -u "$1"', 'configure-path', configure], build, env).strip()
58+
command([bash, configure, '--enable-v3-tls=' + mode, '--disable-examples',
5559
'--prefix=' + prefix.as_posix()], build, env)
5660
command([args.make, '-j' + str(args.jobs)], build, env)
5761
command([args.make, 'install'], build, env)

‎scripts/ci-macos-toolchain.sh‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,20 @@ llvm_prefix=$(brew --prefix llvm)
55
toolchain_dir="$RUNNER_TEMP/v3-toolchain"
66
mkdir -p "$toolchain_dir"
77
{
8-
printf '#!/usr/bin/env bash\nexec '
9-
printf '%q ' "$llvm_prefix/bin/clang++" -stdlib=libc++ -D_LIBCPP_DISABLE_AVAILABILITY \
10-
"-L$llvm_prefix/lib/c++" "-Wl,-rpath,$llvm_prefix/lib/c++"
11-
printf '"$@"\n'
8+
printf '#!/usr/bin/env bash\nruntime_flags=('
9+
printf '%q ' "-L$llvm_prefix/lib/c++" "-Wl,-rpath,$llvm_prefix/lib/c++"
10+
printf ')\n'
11+
cat <<'SH'
12+
# Relocatable links and compile-only actions cannot use a runtime search path.
13+
for argument in "$@"; do
14+
case "$argument" in
15+
-c|-E|-S|-fsyntax-only|-r|-Wl,-r|-static|--version) runtime_flags=(); break ;;
16+
esac
17+
done
18+
SH
19+
printf 'exec '
20+
printf '%q ' "$llvm_prefix/bin/clang++" -stdlib=libc++ -D_LIBCPP_DISABLE_AVAILABILITY
21+
printf '"${runtime_flags[@]}" "$@"\n'
1222
} > "$toolchain_dir/clang++"
1323
chmod +x "$toolchain_dir/clang++"
1424
cat > "$toolchain_dir/cancellation.cpp" <<'CPP'

‎scripts/test-v3-installed-consumer.py‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,13 @@ def setUp(self):
4242
self.args = SimpleNamespace(compiler='selected-cxx', cc='selected-cc',
4343
prerequisite_prefix=[], pkg_config_dir=[], provider_prefix=self.root, optimized=False)
4444
self.popen = subprocess.Popen
45+
self.converted_configure = (self.root / 'configure').as_posix()
46+
self.conversion_requests = []
4547

4648
def create_process(self, argv, **options):
49+
if argv[1:3] == ['-c', 'cygpath -u "$1"']:
50+
self.conversion_requests.append(argv[-1])
51+
return self.popen([sys.executable, '-c', f'print({self.converted_configure!r})'], **options)
4752
# Model only executable lookup at the OS boundary: unqualified bash
4853
# selects the competing Windows system shell before the selected PATH.
4954
selected = str(argv[0]) == str(self.bash)
@@ -56,14 +61,27 @@ def test_windows_configure_uses_absolute_bash_from_selected_path(self):
5661
patch.object(subprocess, 'Popen', side_effect=self.create_process):
5762
with self.assertRaisesRegex(RuntimeError, 'selected MSYS shell'):
5863
self.runner.package_lane(self.args, self.root, self.output, 'no', matrix.load())
59-
record = json.loads((self.output / 'tls-no/commands.json').read_text())[0]
64+
record = json.loads((self.output / 'tls-no/commands.json').read_text())[-1]
6065
self.assertEqual(record['argv'][0], str(self.bash))
6166
self.assertEqual(record['argv'][1], (self.root / 'configure').as_posix())
6267
self.assertEqual(record['environment']['PATH'], str(self.selected))
6368
self.assertEqual(record['environment']['CXX'], 'selected-cxx')
6469
self.assertEqual(record['environment']['CC'], 'selected-cc')
6570
self.assertEqual(record['environment']['CPPFLAGS'], '')
6671

72+
def test_windows_configure_uses_msys_source_path_without_changing_native_prefix(self):
73+
source = self.root / 'D:' / 'selected source'
74+
self.converted_configure = '/d/selected source/configure'
75+
with patch.object(self.runner.platform, 'system', return_value='Windows'), \
76+
patch.dict(os.environ, PATH=str(self.selected)), \
77+
patch.object(subprocess, 'Popen', side_effect=self.create_process):
78+
with self.assertRaisesRegex(RuntimeError, 'selected MSYS shell'):
79+
self.runner.package_lane(self.args, source, self.output, 'no', matrix.load())
80+
records = json.loads((self.output / 'tls-no/commands.json').read_text())
81+
self.assertEqual(records[-1]['argv'][1], self.converted_configure)
82+
self.assertEqual(self.conversion_requests, [(source / 'configure').as_posix()])
83+
self.assertEqual(records[-1]['argv'][-1], '--prefix=' + (self.output / 'tls-no/prefix').as_posix())
84+
6785
def test_windows_missing_path_bash_fails_before_launching_competing_shell(self):
6886
self.bash.unlink()
6987
with patch.object(self.runner.platform, 'system', return_value='Windows'), \

‎scripts/test-v3-installed-package.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
import importlib.util
44
import json
55
import os
6+
import shlex
67
from pathlib import Path
78
import subprocess
89
import tempfile
910
import unittest
1011
from unittest.mock import patch
1112

13+
CXX = os.environ.get('CXX', 'c++')
14+
1215
spec = importlib.util.spec_from_file_location('audit', Path(__file__).with_name('audit-v3-installed-package.py'))
1316
audit = importlib.util.module_from_spec(spec)
1417
spec.loader.exec_module(audit)
@@ -85,7 +88,7 @@ def inspect(command, **kwargs):
8588
with patch.object(audit, '__file__', str(source / 'scripts/audit-v3-installed-package.py')), \
8689
patch.object(audit.platform, 'system', return_value='Darwin'), \
8790
patch.object(subprocess, 'run', side_effect=inspect):
88-
return audit.audit(prefix, build, consumer, 'no', mode, self.root / 'provider')
91+
return audit.audit(prefix, build, consumer, 'no', mode, self.root / 'provider', compiler=CXX)
8992

9093
def test_strict_rejects_configure_flags_probes_and_generated_definitions(self):
9194
for text in ('AC_CHECK_HEADER([microhttpd.h])', 'AC_CHECK_LIB([microhttpd], [MHD_start_daemon])',
@@ -231,7 +234,7 @@ def test_installed_gated_fragment_uses_umbrella_without_internal_macros(self):
231234
(include / 'httpserver.hpp').write_text('#define _HTTPSERVER_HPP_INSIDE_\n#include <httpserver/fragment.hpp>\n#undef _HTTPSERVER_HPP_INSIDE_\n')
232235
(include / 'httpserverpp').symlink_to('httpserver.hpp')
233236
(include / 'httpserver/fragment.hpp').write_text('#ifndef _HTTPSERVER_HPP_INSIDE_\n#error umbrella required\n#endif\nstruct fragment {};\n')
234-
headers, violations = audit.audit_headers(self.root / 'prefix', self.root / 'source', self.root / 'build', 'c++')
237+
headers, violations = audit.audit_headers(self.root / 'prefix', self.root / 'source', self.root / 'build', CXX)
235238
self.assertEqual([], violations)
236239
self.assertIn('httpserver/fragment.hpp', headers['declaration_sha256'])
237240
self.assertIn('httpserver.hpp', headers['include_traces'])
@@ -241,7 +244,7 @@ def test_installed_gated_fragment_uses_umbrella_without_internal_macros(self):
241244

242245
def test_native_public_api_is_available_through_umbrella(self):
243246
source = Path(__file__).resolve().parent.parent
244-
audit.run(['c++', '-std=c++20', '-fsyntax-only', '-x', 'c++', '-I' + str(source / 'src'), '-'],
247+
audit.run(shlex.split(CXX) + ['-std=c++20', '-fsyntax-only', '-x', 'c++', '-I' + str(source / 'src'), '-'],
245248
input='#include <httpserver.hpp>\nhttpserver::server::server_options options;\n')
246249

247250
def test_named_static_suite_rejects_legacy_metadata_closure(self):
@@ -263,7 +266,7 @@ def test_installed_header_symlink_cannot_import_source(self):
263266
(include / 'httpserver.hpp').symlink_to(external)
264267
(include / 'httpserverpp').symlink_to('httpserver.hpp')
265268
with self.assertRaises(audit.AuditError):
266-
audit.audit_headers(self.root / 'prefix', self.root / 'source', self.root / 'build', 'c++')
269+
audit.audit_headers(self.root / 'prefix', self.root / 'source', self.root / 'build', CXX)
267270

268271
def test_runtime_name_outside_platform_location_is_rejected(self):
269272
graph = self.graph({'consumer': ['libc++.1.dylib'], 'libc++.1.dylib': []})
@@ -339,7 +342,7 @@ def test_pe_resolution_uses_explicit_roots_and_rejects_msys(self):
339342
(root / 'libstdc++-6.dll').touch()
340343
with patch.object(audit.platform, 'system', return_value='Windows'), \
341344
patch.dict('os.environ', {'V3_PACKAGE_DLL_DIRS': str(root), 'V3_PACKAGE_TOOLCHAIN': str(root)}):
342-
self.assertEqual(str(root / 'libstdc++-6.dll'), audit.resolve_pe('LIBSTDC++-6.DLL'))
345+
self.assertEqual(str((root / 'libstdc++-6.dll').resolve()), audit.resolve_pe('LIBSTDC++-6.DLL'))
343346
self.assertTrue(audit.runtime(str(root / 'libstdc++-6.dll')))
344347
with self.assertRaises(audit.AuditError):
345348
audit.resolve_pe('unresolved.dll')
@@ -357,7 +360,7 @@ def test_freebsd_runtime_leaf_with_dynamic_section(self):
357360
def test_current_umbrella_has_no_backend_types(self):
358361
# Compile the real current umbrella as a consumer, comments removed by cpp.
359362
source = Path(__file__).resolve().parent.parent
360-
result = audit.run(['c++', '-std=c++20', '-E', '-x', 'c++', '-I' + str(source / 'src'), '-'],
363+
result = audit.run(shlex.split(CXX) + ['-std=c++20', '-E', '-x', 'c++', '-I' + str(source / 'src'), '-'],
361364
input='#include <httpserver.hpp>\n')
362365
self.assertEqual([], audit.header_violations(result, []))
363366

‎scripts/test-v3-platform-ci.py‎

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import argparse
55
import importlib.util
66
import os
7+
import shlex
8+
import shutil
9+
import sys
710
from pathlib import Path
811
import subprocess
912
import tempfile
@@ -65,6 +68,9 @@ def prepare(self, root, compiler_status=0):
6568
import json, os, pathlib, sys
6669
with open(os.environ['INVOCATIONS'], 'a') as stream:
6770
stream.write(json.dumps(sys.argv[1:]) + '\\n')
71+
if any(mode in sys.argv for mode in ('-r', '-Wl,-r', '-c', '-S', '-E', '-fsyntax-only', '-static')) and any(arg.startswith('-Wl,-rpath,') for arg in sys.argv):
72+
sys.stderr.write('rpath requires a final dynamic link\\n')
73+
sys.exit(73)
6874
if '-o' in sys.argv:
6975
output = pathlib.Path(sys.argv[sys.argv.index('-o') + 1])
7076
output.write_text('#!/bin/sh\\nexit 0\\n')
@@ -91,13 +97,36 @@ def test_wrapper_selects_matching_library_and_preserves_arguments(self):
9197
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
9298
selected = dict(line.split('=', 1) for line in Path(env['GITHUB_ENV']).read_text().splitlines())
9399
self.assertEqual(selected['V3_CC'], str(llvm / 'bin/clang'))
94-
result = subprocess.run([selected['V3_CXX'], '-c', 'path with spaces.cpp'], env=env)
100+
result = subprocess.run([selected['V3_CXX'], 'path with spaces.cpp', '-o', str(root / 'consumer')], env=env)
95101
self.assertEqual(result.returncode, 0)
96102
calls = [json.loads(line) for line in Path(env['INVOCATIONS']).read_text().splitlines()]
97103
self.assertIn('-std=c++20', calls[1])
98104
self.assertIn('-L' + str(llvm / 'lib/c++'), calls[1])
99105
self.assertIn('-Wl,-rpath,' + str(llvm / 'lib/c++'), calls[1])
100-
self.assertEqual(calls[-1][-2:], ['-c', 'path with spaces.cpp'])
106+
self.assertEqual(calls[-1][-3:], ['path with spaces.cpp', '-o', str(root / 'consumer')])
107+
result = subprocess.run([selected['V3_CXX'], '-dynamiclib', 'input.o'], env=env)
108+
self.assertEqual(result.returncode, 0)
109+
calls = [json.loads(line) for line in Path(env['INVOCATIONS']).read_text().splitlines()]
110+
self.assertIn('-L' + str(llvm / 'lib/c++'), calls[-1])
111+
self.assertIn('-Wl,-rpath,' + str(llvm / 'lib/c++'), calls[-1])
112+
113+
def test_nonfinal_modes_do_not_receive_runtime_linker_flags(self):
114+
with tempfile.TemporaryDirectory() as directory:
115+
root = Path(directory)
116+
result, env, llvm = self.prepare(root)
117+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
118+
selected = dict(line.split('=', 1) for line in Path(env['GITHUB_ENV']).read_text().splitlines())
119+
for mode in ('-r', '-Wl,-r', '-c', '-S', '-E', '-fsyntax-only', '-static'):
120+
with self.subTest(mode=mode):
121+
result = subprocess.run([selected['V3_CXX'], mode, 'path with spaces.cpp'],
122+
env=env, capture_output=True, text=True)
123+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
124+
calls = [json.loads(line) for line in Path(env['INVOCATIONS']).read_text().splitlines()]
125+
self.assertIn('-stdlib=libc++', calls[-1])
126+
self.assertIn('-D_LIBCPP_DISABLE_AVAILABILITY', calls[-1])
127+
self.assertNotIn('-L' + str(llvm / 'lib/c++'), calls[-1])
128+
self.assertNotIn('-Wl,-rpath,' + str(llvm / 'lib/c++'), calls[-1])
129+
self.assertEqual(calls[-1][-2:], [mode, 'path with spaces.cpp'])
101130

102131
def test_missing_cancellation_support_stops_before_exporting_toolchain(self):
103132
with tempfile.TemporaryDirectory() as directory:
@@ -106,5 +135,46 @@ def test_missing_cancellation_support_stops_before_exporting_toolchain(self):
106135
self.assertFalse(Path(env['GITHUB_ENV']).exists())
107136

108137

138+
@unittest.skipIf(os.name == 'nt', 'competing compiler and symlink fixtures use POSIX paths')
139+
class InstalledPackageFixtureEnvironment(unittest.TestCase):
140+
def test_fixture_entrypoint_honors_quoted_cxx_command(self):
141+
with tempfile.TemporaryDirectory() as directory:
142+
root = Path(directory)
143+
selected = root / 'selected compiler.py'
144+
selected.write_text("""import json, os, sys
145+
if sys.argv[1] != '--selected-toolchain':
146+
sys.exit(72)
147+
with open(os.environ['INVOCATIONS'], 'a') as stream:
148+
stream.write(json.dumps(sys.argv[2:]) + '\\n')
149+
os.execv(os.environ['REAL_CXX'], [os.environ['REAL_CXX']] + sys.argv[2:])
150+
""")
151+
competing = root / 'c++'
152+
competing.write_text('#!/bin/sh\nprintf "unselected compiler invoked\\n" >&2\nexit 71\n')
153+
competing.chmod(0o755)
154+
env = dict(os.environ, CXX=shlex.join([sys.executable, str(selected), '--selected-toolchain']),
155+
PATH=str(root) + os.pathsep + os.environ['PATH'],
156+
INVOCATIONS=str(root / 'calls.jsonl'), REAL_CXX=shutil.which('c++'))
157+
result = subprocess.run([sys.executable, str(ROOT / 'scripts/test-v3-installed-package.py'),
158+
'AuditTest.test_native_public_api_is_available_through_umbrella',
159+
'AuditTest.test_current_umbrella_has_no_backend_types'],
160+
env=env, capture_output=True, text=True)
161+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
162+
calls = [json.loads(line) for line in Path(env['INVOCATIONS']).read_text().splitlines()]
163+
self.assertTrue(any('-fsyntax-only' in call for call in calls))
164+
self.assertTrue(any('-E' in call for call in calls))
165+
166+
def test_pe_fixture_accepts_canonical_path_under_symlinked_tmpdir(self):
167+
with tempfile.TemporaryDirectory() as directory:
168+
root = Path(directory)
169+
target = root / 'real'
170+
target.mkdir()
171+
alias = root / 'alias'
172+
alias.symlink_to(target, target_is_directory=True)
173+
result = subprocess.run([sys.executable, str(ROOT / 'scripts/test-v3-installed-package.py'),
174+
'AuditTest.test_pe_resolution_uses_explicit_roots_and_rejects_msys'],
175+
env=dict(os.environ, TMPDIR=str(alias)), capture_output=True, text=True)
176+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
177+
178+
109179
if __name__ == '__main__':
110180
unittest.main()

0 commit comments

Comments
 (0)