55import importlib .util
66import io
77import json
8+ import os
9+ import shlex
10+ import subprocess
811from pathlib import Path
912import sys
1013import tarfile
@@ -26,7 +29,7 @@ def setUp(self):
2629 self .assertIsNotNone (adapter , 'packaged adapter implementation missing' )
2730 self .temp = tempfile .TemporaryDirectory ()
2831 self .addCleanup (self .temp .cleanup )
29- self .root = Path (self .temp .name )
32+ self .root = Path (self .temp .name ). resolve ()
3033 self .source = self .root / 'source'
3134 self .source .mkdir ()
3235 (self .source / 'unit.cpp' ).write_text ('public source\n ' )
@@ -99,17 +102,22 @@ def provenance(self):
99102 unit = directory / 'consumer.cpp' ; unit .write_text ('public source\n ' )
100103 executable = directory / 'static' ; executable .write_text ('consumer' )
101104 depfile = directory / 'static-0.d' ; depfile .write_text (f'static.o: { unit } { header } \n ' )
102- linkmap = directory / 'static.map' ; linkmap .write_text (str (library ) + '(public.o)\n LOAD ' + str (library ) + '\n ' )
105+ linkmap = directory / 'static.map'
106+ linkmap .write_text (('# Object files:\n [ 1] ' + str (library ) + '[1](public.o)\n # Sections:\n ' )
107+ if sys .platform == 'darwin' else str (library ) + '(public.o)\n LOAD ' + str (library ) + '\n ' )
108+ compiler = os .environ .get ('CXX' , 'c++' )
109+ compiler_argv = shlex .split (compiler )
110+ map_option = '-Wl,-map,' if sys .platform == 'darwin' else '-Wl,-Map,'
103111 obj = directory / 'static-0.o' ; obj .write_text ('object' )
104112 log = self .root / 'command.log' ; log .write_text ('compile/link ok' )
105- commands = [dict (argv = [ 'c++' , '-std=c++20' , '-MMD' , '-MF' , str (depfile ), '-c' , str (unit ), '-o' , str (obj )],
113+ commands = [dict (argv = compiler_argv + [ '-std=c++20' , '-MMD' , '-MF' , str (depfile ), '-c' , str (unit ), '-o' , str (obj )],
106114 exit = 0 , log = str (log ), sha256 = bench .artifact (log )['sha256' ], bytes = log .stat ().st_size ),
107- dict (argv = [ 'c++' , str (obj ), str (library ), '-Wl,-Map,' + str (linkmap ), '-o' , str (executable )],
115+ dict (argv = compiler_argv + [ str (obj ), str (library ), map_option + str (linkmap ), '-o' , str (executable )],
108116 exit = 0 , log = str (log ), sha256 = bench .artifact (log )['sha256' ], bytes = log .stat ().st_size )]
109117 compiled = dict (private_test_headers = False , sources = {str (unit ): bench .artifact (unit )['sha256' ]},
110118 dependencies = [str (unit ), str (header )], dependency_sha256 = {str (p ): bench .artifact (p )['sha256' ] for p in (unit , header )},
111119 artifact_sha256 = bench .artifact (executable )['sha256' ], link_map = str (linkmap ))
112- identity = dict (source = str (self .source ), build = str (self .root / 'build' ), prefix = str (prefix ), compiler = 'c++' ,
120+ identity = dict (source = str (self .source ), build = str (self .root / 'build' ), prefix = str (prefix ), compiler = compiler ,
113121 archive = {'members' : {'test/installed/consumer.cpp' : bench .artifact (unit )['sha256' ]}},
114122 package = {str (p ): bench .artifact (p )['sha256' ] for p in (header , library )})
115123 artifacts = {str (p ): bench .artifact (p ) for p in directory .iterdir () if p .is_file ()}
@@ -154,6 +162,29 @@ def test_empty_linkmap_and_foreign_dependency_are_rejected(self):
154162 artifacts [compiled ['link_map' ]] = bench .artifact (compiled ['link_map' ])
155163 with self .assertRaises (ValueError ): adapter .validate_compile (compiled , 'static' , identity , artifacts , commands )
156164
165+ def test_darwin_link_map_requires_actual_selected_archive_contribution (self ):
166+ from unittest .mock import patch
167+ compiled , identity , artifacts , commands = self .provenance ()
168+ linkmap = Path (compiled ['link_map' ])
169+ archive = str (Path (identity ['prefix' ]) / 'lib/libhttpserver.a' )
170+ foreign = str (self .root / 'foreign/libhttpserver.a' )
171+ commands [1 ]['argv' ][- 3 ] = '-Wl,-map,' + str (linkmap )
172+ with patch ('platform.system' , return_value = 'Darwin' ):
173+ for member in (archive + '(public.o)' , archive + '[12](public.o)' ):
174+ linkmap .write_text ('# Path: consumer\n # Object files:\n [ 0] linker synthesized\n [ 1] ' + member + '\n # Sections:\n # Symbols:\n ' )
175+ artifacts [str (linkmap )] = bench .artifact (linkmap )
176+ adapter .validate_compile (compiled , 'static' , identity , artifacts , commands )
177+ for body in ('' , 'LOAD ' + archive , archive + '(public.o)' ,
178+ '# Object files:\n [ 1] ' + foreign + '[1](public.o)\n # Sections:\n ' ,
179+ '# Object files:\n [ 1] consumer.o\n # Sections:\n ' + archive + '(unused.o)' ,
180+ '# Object files:\n [ 1] ' + archive + '[1](public.o)\n [ 2] ' + foreign + '[1](other.o)\n # Sections:\n ' ):
181+ linkmap .write_text (body ); artifacts [str (linkmap )] = bench .artifact (linkmap )
182+ with self .subTest (body = body ), self .assertRaisesRegex (ValueError , 'static archive' ):
183+ adapter .validate_compile (compiled , 'static' , identity , artifacts , commands )
184+ commands [1 ]['argv' ][- 3 ] = '-Wl,-Map,' + str (linkmap )
185+ with self .assertRaisesRegex (ValueError , 'link map command' ):
186+ adapter .validate_compile (compiled , 'static' , identity , artifacts , commands )
187+
157188 def test_artifact_catalog_rejects_pruning_and_command_tampering (self ):
158189 from release_package_provenance import catalog , validate_catalog
159190 output = self .root / 'evidence' ; output .mkdir ()
@@ -363,6 +394,23 @@ def test_each_public_family_requires_its_compile_and_dependency_records(self):
363394 bad = copy .deepcopy (child ); bad .pop ('independent_client' )
364395 with self .assertRaises (ValueError ): provenance .validate_child_provenance (bad , identity , {}, [], family )
365396
397+ def test_static_archive_fixture_uses_selected_quoted_compiler (self ):
398+ compiler = shlex .split (os .environ .get ('CXX' , 'c++' ))
399+ wrapper = self .root / 'selected compiler.py'
400+ marker = self .root / 'compiler-commands.jsonl'
401+ wrapper .write_text ('import json, os, sys\n '
402+ 'with open(' + repr (str (marker )) + ', "a") as stream: stream.write(json.dumps(sys.argv[1:]) + "\\ n")\n '
403+ 'argv = ' + repr (compiler ) + '\n os.execvp(argv[0], argv + sys.argv[1:])\n ' )
404+ result = subprocess .run ([sys .executable , str (Path (__file__ ).resolve ()),
405+ 'AdapterContracts.test_unused_selected_static_archive_cannot_supply_link_identity' ],
406+ env = dict (os .environ , CXX = shlex .join ([sys .executable , str (wrapper )])),
407+ capture_output = True , text = True , timeout = 30 )
408+ self .assertEqual (result .returncode , 0 , result .stdout + result .stderr )
409+ self .assertTrue (marker .is_file (), 'archive fixture bypassed the selected compiler' )
410+ commands = [json .loads (line ) for line in marker .read_text ().splitlines ()]
411+ self .assertEqual (sum ('-c' in argv for argv in commands ), 3 )
412+ self .assertEqual (sum ('-c' not in argv for argv in commands ), 2 )
413+
366414 def test_unused_selected_static_archive_cannot_supply_link_identity (self ):
367415 import subprocess
368416 compiled , identity , artifacts , commands = self .provenance ()
@@ -377,16 +425,17 @@ def test_unused_selected_static_archive_cannot_supply_link_identity(self):
377425 source = folder / 'implementation.cpp' ; source .write_text ('int implementation() { return %d; }\n ' % value )
378426 obj = folder / 'implementation.o' ; archive = folder / 'libhttpserver.a'
379427 archive .unlink (missing_ok = True )
380- subprocess .run ([ 'c++' , '-c' , str (source ), '-o' , str (obj )], check = True , capture_output = True )
428+ subprocess .run (shlex . split ( identity [ 'compiler' ]) + [ '-c' , str (source ), '-o' , str (obj )], check = True , capture_output = True )
381429 subprocess .run (['ar' , 'rcs' , str (archive ), str (obj )], check = True , capture_output = True )
382430 libraries .append (str (archive ))
383- commands [0 ]['argv' ].insert (2 , '-I' + str (Path (identity ['prefix' ]) / 'include' ))
384- commands [1 ]['argv' ][2 :3 ] = libraries
385- for command in commands : subprocess .run (command ['argv' ], check = True , capture_output = True )
431+ from package_commands import Commands
432+ from package_integration import compile_probe
433+ command = Commands (directory )
434+ compiled = compile_probe (identity ['compiler' ], [unit ], ['-I' + str (Path (identity ['prefix' ]) / 'include' )],
435+ libraries , directory / 'static' , command , os .environ , self .source ,
436+ Path (identity ['build' ]), Path (identity ['prefix' ]), False )
437+ commands = command .records
386438 self .assertEqual (subprocess .run ([str (directory / 'static' )]).returncode , 2 )
387- compiled ['sources' ] = {str (unit ): bench .artifact (unit )['sha256' ]}
388- compiled ['dependency_sha256' ] = {str (p ): bench .artifact (p )['sha256' ] for p in (unit , header )}
389- compiled ['artifact_sha256' ] = bench .artifact (directory / 'static' )['sha256' ]
390439 identity ['archive' ]['members' ]['test/installed/consumer.cpp' ] = compiled ['sources' ][str (unit )]
391440 identity ['package' ] = {p : bench .artifact (p )['sha256' ] for p in identity ['package' ]}
392441 artifacts = {str (p ): bench .artifact (p ) for p in directory .iterdir () if p .is_file ()}
0 commit comments