Skip to content

Commit 98dfdc4

Browse files
committed
Merge FEATURE-ci-darwin-selected-tools: retain selected native tools and identity
2 parents f37e7c7 + a2f3d92 commit 98dfdc4

6 files changed

Lines changed: 254 additions & 12 deletions

File tree

‎.github/workflows/v3-package-validation.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
steps:
5050
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
5151
- name: Prerequisites
52-
run: brew install autoconf automake libtool pkg-config python curl llvm
52+
run: brew install autoconf automake libtool pkg-config python curl llvm make
5353
- name: Select complete C++20 compiler and runtime
5454
run: bash scripts/ci-macos-toolchain.sh
5555
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
@@ -72,7 +72,7 @@ jobs:
7272
if ! python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",443)); s.close()'; then
7373
prefix=(sudo env "PATH=$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory "GIT_CONFIG_VALUE_0=$PWD")
7474
fi
75-
"${prefix[@]}" python3 scripts/check-v3-installed-consumer.py --output "$RUNNER_TEMP/package-evidence/matrix" --provider-prefix "$RUNNER_TEMP/package-evidence/prerequisites/provider" --pkg-config-dir "$(brew --prefix)/share/pkgconfig" --client-python "$RUNNER_TEMP/package-evidence/clients/bin/python" --node node --compiler "$V3_CXX" --cc "$V3_CC" --toolchain-prefix "$V3_PACKAGE_TOOLCHAIN" --jobs 2
75+
"${prefix[@]}" python3 scripts/check-v3-installed-consumer.py --output "$RUNNER_TEMP/package-evidence/matrix" --provider-prefix "$RUNNER_TEMP/package-evidence/prerequisites/provider" --pkg-config-dir "$(brew --prefix)/share/pkgconfig" --client-python "$RUNNER_TEMP/package-evidence/clients/bin/python" --node node --compiler "$V3_CXX" --cc "$V3_CC" --toolchain-prefix "$V3_PACKAGE_TOOLCHAIN" --make gmake --jobs 2
7676
- name: Make privileged proof readable by artifact upload
7777
if: always()
7878
run: |

‎.github/workflows/v3-release-acceptance.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ jobs:
100100
with:
101101
ref: ${{ needs.prepare.outputs.commit }}
102102
persist-credentials: false
103-
- run: brew install autoconf automake libtool pkg-config python curl llvm
103+
- run: brew install autoconf automake libtool pkg-config python curl llvm make
104104
- name: Select complete C++20 compiler and runtime
105105
run: bash scripts/ci-macos-toolchain.sh
106106
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
@@ -122,7 +122,7 @@ jobs:
122122
if ! python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",443)); s.close()'; then
123123
prefix=(sudo env "PATH=$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory "GIT_CONFIG_VALUE_0=$PWD")
124124
fi
125-
"${prefix[@]}" python3 scripts/run-v3-release-ci.py platform --selection "$RUNNER_TEMP/selected/selected.json" --archive "$RUNNER_TEMP/selected/"libhttpserver-*.tar.gz --provider-prefix "$RUNNER_TEMP/prerequisites/provider" --client-python "$RUNNER_TEMP/clients/bin/python" --node node --compiler "$V3_CXX" --cc "$V3_CC" --toolchain-prefix "$V3_PACKAGE_TOOLCHAIN" --output "$RUNNER_TEMP/platform" --pkg-config-dir "$(brew --prefix)/share/pkgconfig"
125+
"${prefix[@]}" python3 scripts/run-v3-release-ci.py platform --selection "$RUNNER_TEMP/selected/selected.json" --archive "$RUNNER_TEMP/selected/"libhttpserver-*.tar.gz --provider-prefix "$RUNNER_TEMP/prerequisites/provider" --client-python "$RUNNER_TEMP/clients/bin/python" --node node --compiler "$V3_CXX" --cc "$V3_CC" --toolchain-prefix "$V3_PACKAGE_TOOLCHAIN" --make gmake --output "$RUNNER_TEMP/platform" --pkg-config-dir "$(brew --prefix)/share/pkgconfig"
126126
- name: Make privileged proof readable by artifact upload
127127
if: always()
128128
run: |

‎scripts/run-v3-release-ci.py‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
from pathlib import Path
88
import platform
9+
import shlex
910
import shutil
1011
import sys
1112
import tarfile
@@ -57,7 +58,8 @@ def platform_proof(args):
5758
require(release['source'] == selected['source'] and release['files_sha256'] == selected['files_sha256'], 'selected snapshot/tooling differs')
5859
source = Path(release['source_root'])
5960
identity_path = args.output / 'artifact-identity.json'; identity_path.write_text(json.dumps(release))
60-
env = dict(os.environ, V3_RELEASE_IDENTITY=str(identity_path),
61+
env = dict(os.environ, CXX=args.compiler, CC=args.cc, MAKE=args.make,
62+
V3_RELEASE_IDENTITY=str(identity_path),
6163
V3_TLS_CFLAGS='-I' + (args.provider_prefix / 'include').as_posix(),
6264
V3_TLS_LIBS='-L' + (args.provider_prefix / 'lib').as_posix() + ' -lssl -lcrypto')
6365
if args.toolchain_prefix:
@@ -94,9 +96,16 @@ def platform_proof(args):
9496
files += [args.provider_prefix / 'include/openssl/opensslv.h', args.archive]
9597
for receipt_path in package.rglob('*.json'):
9698
files += list(referenced_files(json.loads(receipt_path.read_text())))
97-
for tool in (args.compiler, args.cc, args.make, args.client_python, args.node, 'git'):
98-
selected_tool = shutil.which(str(tool)); require(selected_tool, 'selected tool unavailable: ' + str(tool))
99+
compiler_tools = []
100+
for compiler in (args.compiler, args.cc):
101+
compiler_tools.append([compiler] if shutil.which(compiler, path=env['PATH']) else shlex.split(compiler))
102+
for command in compiler_tools + [[args.make], [args.client_python], [args.node], ['git']]:
103+
require(command, 'empty selected tool command')
104+
selected_tool = shutil.which(str(command[0]), path=env['PATH'])
105+
require(selected_tool, 'selected tool unavailable: ' + str(command[0]))
99106
files.append(Path(selected_tool))
107+
# Interpreter/cache-wrapper arguments can name the compiler script.
108+
files += [source / argument for argument in command[1:] if (source / argument).is_file()]
100109
files = sorted(set(files))
101110
artifacts = [gate.artifact(path) for path in files]
102111
row = dict(id=platform.system(), status='passed', archive_sha256=selected['sha256'], identity=expected,

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,15 @@ def create_process(argv, **options):
319319

320320

321321
class CIWorkflows(unittest.TestCase):
322+
def test_macos_provisions_gnu_make_before_selecting_it(self):
323+
for filename, job in (('v3-package-validation.yml', 'macos'), ('v3-release-acceptance.yml', 'darwin')):
324+
with self.subTest(workflow=filename):
325+
steps = workflow(filename)['jobs'][job]['steps']
326+
provision = next(i for i, step in enumerate(steps) if 'brew install ' in step.get('run', ''))
327+
self.assertIn('make', shlex.split(steps[provision]['run'])[2:])
328+
invocation = next(i for i, step in enumerate(steps) if '"${prefix[@]}" python3' in step.get('run', ''))
329+
self.assertLess(provision, invocation)
330+
322331
def test_artifact_api_head_is_bound_to_trusted_event_head(self):
323332
lane = workflow('v3-release-acceptance.yml')
324333
self.assertEqual(lane.get('env', {}).get('WORKFLOW_HEAD_SHA'),

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

Lines changed: 136 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ class SelectedPlatformEnvironment(unittest.TestCase):
102102
class NativeChecksComplete(Exception):
103103
pass
104104

105-
def native_checks(self, root, inherited_prefix=None, selected=True, dependency_selected=True):
105+
def native_checks(self, root, inherited_prefix=None, selected=True, dependency_selected=True,
106+
selected_tools=False, retain_tools=False):
106107
"""Run the archive orchestrator, real child/make and the production audit.
107108
108109
Only archive identity, the expensive package producer and native inspectors
@@ -162,7 +163,7 @@ def native_checks(self, root, inherited_prefix=None, selected=True, dependency_s
162163
source={}, files_sha256={})))
163164
args = argparse.Namespace(selection=selection, output=root / 'output', archive=root / 'source.tar.gz',
164165
provider_prefix=root / 'provider', compiler=str(sdk / 'bin/clang++'), cc='/llvm/bin/clang',
165-
make=shutil.which('gmake') or shutil.which('make'), jobs=2, client_python=sys.executable,
166+
make=os.environ.get('MAKE') or shutil.which('gmake') or shutil.which('make'), jobs=2, client_python=sys.executable,
166167
node='node', pkg_config_dir=[], toolchain_prefix=sdk if selected else None)
167168
release = dict(source_root=str(source), source={}, files_sha256={})
168169
counts = dict(tests=1, checks=1)
@@ -171,19 +172,120 @@ def native_checks(self, root, inherited_prefix=None, selected=True, dependency_s
171172
env.pop('V3_PACKAGE_TOOLCHAIN', None)
172173
if inherited_prefix is not None:
173174
env['V3_PACKAGE_TOOLCHAIN'] = str(inherited_prefix)
175+
if selected_tools:
176+
compiler = root / 'selected compiler.py'
177+
compiler.write_text(textwrap.dedent('''\
178+
import json, os, shlex, sys
179+
assert sys.argv[1:3] == ['--selected-toolchain', 'argument with spaces'], sys.argv
180+
with open(os.environ['COMPILER_CALLS'], 'a') as stream:
181+
stream.write(json.dumps(sys.argv[3:]) + '\\n')
182+
command = shlex.split(os.environ['CALLER_CXX']) + sys.argv[3:]
183+
os.execvp(command[0], command)
184+
'''))
185+
make = root / 'selected-make'
186+
make.symlink_to(shutil.which('gmake') or shutil.which('make'))
187+
python = root / 'selected python'
188+
python.symlink_to(sys.executable)
189+
args.compiler = shlex.join([str(python), str(compiler), '--selected-toolchain', 'argument with spaces'])
190+
args.cc = str(root / 'selected cc')
191+
args.make = str(make)
192+
probe = root / 'compile probe.py'
193+
probe.write_text(textwrap.dedent('''\
194+
import os, shlex, subprocess
195+
assert os.environ['CC'] == os.environ['EXPECTED_CC'], os.environ.get('CC')
196+
assert os.environ['MAKE'] == os.environ['EXPECTED_MAKE'], os.environ.get('MAKE')
197+
subprocess.run(shlex.split(os.environ['CXX']) + ['-std=c++20', '-fsyntax-only',
198+
'-x', 'c++', '-I' + os.environ['PUBLIC_HEADERS'], '-'],
199+
input='#include <httpserver.hpp>\\nhttpserver::server::server_options options;\\n',
200+
text=True, check=True)
201+
'''))
202+
for mode in ('no', 'yes'):
203+
makefile = root / 'fixture' / ('tls-' + mode) / 'build/Makefile'
204+
with makefile.open('a') as stream:
205+
stream.write('check: selected-tools\nselected-tools:\n\t' + shlex.join([sys.executable, str(probe)]) + '\n')
206+
env.update(CXX='unselected-cxx', CC='unselected-cc', MAKE='unselected-make',
207+
EXPECTED_CC=args.cc, EXPECTED_MAKE=args.make, PUBLIC_HEADERS=str(ROOT / 'src'),
208+
CALLER_CXX=os.environ.get('CXX', 'c++'), COMPILER_CALLS=str(root / 'compiler-calls.jsonl'))
209+
if retain_tools:
210+
compiler_alias = root / 'compiler alias.py'
211+
compiler_alias.symlink_to(compiler)
212+
args.compiler = shlex.join([str(python), str(compiler_alias), '--selected-toolchain', 'argument with spaces'])
213+
cc = Path(args.cc)
214+
cc.symlink_to(shutil.which('cc'))
215+
args.cc = shlex.join([str(cc), '-DSELECTED_CC=1'])
216+
env['EXPECTED_CC'] = args.cc
217+
# Exercise a spaced make executable at the retention boundary. GNU
218+
# make's recursive MAKE variable needs a shell-quoted command.
219+
make_alias = root / 'selected make'
220+
make_alias.symlink_to(make)
221+
args.make = str(make_alias)
222+
for mode in ('no', 'yes'):
223+
makefile = root / 'fixture' / ('tls-' + mode) / 'build/Makefile'
224+
with makefile.open('a') as stream:
225+
stream.write('MAKE := ' + shlex.quote(str(make_alias)) + '\n')
226+
env['EXPECTED_MAKE'] = shlex.quote(str(make_alias))
227+
args.node = sys.executable
228+
args.archive.touch()
229+
header = args.provider_prefix / 'include/openssl/opensslv.h'
230+
header.parent.mkdir(parents=True)
231+
header.write_text('selected provider header')
232+
gate.artifact = lambda path: dict(path=str(path), bytes=path.stat().st_size,
233+
sha256=hashlib.sha256(path.read_bytes()).hexdigest())
174234
with patch.object(entrypoint.platform, 'system', return_value='FreeBSD'), \
175235
patch.object(entrypoint, 'validate_request'), \
176236
patch.object(entrypoint.identity, 'source_identity', return_value={}), \
177237
patch.object(entrypoint, 'selected_archive', return_value=release), \
178238
patch.object(entrypoint, 'gate_runner', return_value=gate), \
179239
patch.object(entrypoint.checks, 'validate_package', return_value=counts), \
180-
patch.object(entrypoint.checks, 'automake_result', side_effect=[counts, self.NativeChecksComplete()]), \
240+
patch.object(entrypoint.checks, 'automake_result', side_effect=[counts, counts if retain_tools else self.NativeChecksComplete()]), \
241+
patch.object(entrypoint, 'validate_origin'), \
181242
patch.dict(os.environ, env, clear=True):
182243
try:
183244
entrypoint.platform_proof(args)
184245
finally:
185246
self.assertEqual(os.environ, env, 'archive proof must keep its selected SDK environment local')
186247

248+
@unittest.skipIf(os.name == 'nt', 'selected tools fixture requires POSIX scripts and GNU make')
249+
def test_explicit_tools_reach_real_make_and_python_compiler_checks(self):
250+
with tempfile.TemporaryDirectory() as directory:
251+
root = Path(directory)
252+
with self.assertRaises(self.NativeChecksComplete):
253+
self.native_checks(root, selected_tools=True)
254+
calls = [json.loads(line) for line in (root / 'compiler-calls.jsonl').read_text().splitlines()]
255+
self.assertEqual(len(calls), 2, 'both TLS modes must compile through the selected CXX command')
256+
self.assertTrue(all('-fsyntax-only' in call for call in calls))
257+
258+
@unittest.skipIf(os.name == 'nt', 'selected make fixture requires POSIX scripts and GNU make')
259+
def test_native_fixture_uses_callers_selected_make_for_test_enumeration(self):
260+
with tempfile.TemporaryDirectory() as directory:
261+
root = Path(directory)
262+
make = root / 'caller-make'
263+
real_make = os.environ.get('MAKE') or shutil.which('gmake') or shutil.which('make')
264+
calls = root / 'make-calls.jsonl'
265+
make.write_text('#!' + sys.executable + '\nimport json, os, sys\n' +
266+
'with open(' + repr(str(calls)) + ', "a") as stream: stream.write(json.dumps(sys.argv[1:]) + "\\n")\n' +
267+
'os.execv(' + repr(real_make) + ', [' + repr(real_make) + '] + sys.argv[1:])\n')
268+
make.chmod(0o755)
269+
with patch.dict(os.environ, MAKE=str(make)), self.assertRaises(self.NativeChecksComplete):
270+
self.native_checks(root)
271+
self.assertTrue(calls.is_file(), 'native fixture bypassed the caller-selected make')
272+
invocations = [json.loads(line) for line in calls.read_text().splitlines()]
273+
self.assertEqual(sum('--eval' in argv for argv in invocations), 2)
274+
275+
@unittest.skipIf(os.name == 'nt', 'selected tool retention fixture uses POSIX executable aliases')
276+
def test_archive_retains_compound_compilers_and_spaced_make_exact_bytes(self):
277+
with tempfile.TemporaryDirectory() as directory:
278+
root = Path(directory)
279+
self.native_checks(root, selected_tools=True, retain_tools=True)
280+
sys.path.insert(0, str(ROOT / 'scripts/lib'))
281+
from release_platform import Catalog
282+
catalog = Catalog(root / 'output/bundle/FreeBSD')
283+
for path in (root / 'selected python', root / 'compiler alias.py', root / 'selected compiler.py',
284+
root / 'selected cc', root / 'selected make'):
285+
with self.subTest(path=path):
286+
self.assertEqual(catalog.path(str(path)).read_bytes(), path.read_bytes())
287+
self.assertEqual(catalog.path(str(path.resolve())).read_bytes(), path.read_bytes())
288+
187289
@unittest.skipIf(os.name == 'nt', 'FreeBSD audit fixtures require POSIX scripts and GNU make')
188290
def test_explicit_sdk_reaches_both_later_make_checks(self):
189291
for inherited in (None, '/unselected/sdk'):
@@ -356,6 +458,7 @@ def test_workflows_pass_selected_prefix_even_when_sudo_cleans_environment(self):
356458
self.assertEqual(argv[argv.index('--toolchain-prefix') + 1] if '--toolchain-prefix' in argv else None, str(llvm))
357459
self.assertEqual(argv[argv.index('--compiler') + 1], selected['V3_CXX'])
358460
self.assertEqual(argv[argv.index('--cc') + 1], selected['V3_CC'])
461+
self.assertEqual(argv[argv.index('--make') + 1] if '--make' in argv else None, 'gmake')
359462
if privileged:
360463
self.assertIsNone(captured['toolchain_environment'])
361464

@@ -386,6 +489,30 @@ def test_missing_cancellation_support_stops_before_exporting_toolchain(self):
386489

387490
@unittest.skipIf(os.name == 'nt', 'competing compiler and symlink fixtures use POSIX paths')
388491
class InstalledPackageFixtureEnvironment(unittest.TestCase):
492+
def test_quoted_fixture_preserves_callers_compound_compiler(self):
493+
with tempfile.TemporaryDirectory() as directory:
494+
root = Path(directory)
495+
compiler = root / 'caller compiler.py'
496+
compiler.write_text(textwrap.dedent('''\
497+
import json, os, shlex, sys
498+
assert sys.argv[1:3] == ['--caller-selection', 'argument with spaces'], sys.argv
499+
with open(os.environ['CALLER_INVOCATIONS'], 'a') as stream:
500+
stream.write(json.dumps(sys.argv[3:]) + '\\n')
501+
command = shlex.split(os.environ['CALLER_CXX']) + sys.argv[3:]
502+
os.execvp(command[0], command)
503+
'''))
504+
env = dict(os.environ, CXX=shlex.join([sys.executable, str(compiler), '--caller-selection', 'argument with spaces']),
505+
CALLER_CXX=os.environ.get('CXX') or shlex.join([shutil.which('c++')]),
506+
CALLER_INVOCATIONS=str(root / 'calls.jsonl'))
507+
result = subprocess.run([sys.executable, str(Path(__file__).resolve()),
508+
'InstalledPackageFixtureEnvironment.test_fixture_entrypoint_honors_quoted_cxx_command'],
509+
env=env, capture_output=True, text=True)
510+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
511+
self.assertTrue((root / 'calls.jsonl').exists(), 'quoted fixture bypassed the caller-selected compiler')
512+
calls = [json.loads(line) for line in (root / 'calls.jsonl').read_text().splitlines()]
513+
self.assertTrue(any('-fsyntax-only' in call for call in calls))
514+
self.assertTrue(any('-E' in call for call in calls))
515+
389516
def test_fixture_entrypoint_honors_quoted_cxx_command(self):
390517
with tempfile.TemporaryDirectory() as directory:
391518
root = Path(directory)
@@ -395,14 +522,18 @@ def test_fixture_entrypoint_honors_quoted_cxx_command(self):
395522
sys.exit(72)
396523
with open(os.environ['INVOCATIONS'], 'a') as stream:
397524
stream.write(json.dumps(sys.argv[2:]) + '\\n')
398-
os.execv(os.environ['REAL_CXX'], [os.environ['REAL_CXX']] + sys.argv[2:])
525+
import shlex
526+
command = shlex.split(os.environ['REAL_CXX']) + sys.argv[2:]
527+
os.execvp(command[0], command)
399528
""")
400529
competing = root / 'c++'
401530
competing.write_text('#!/bin/sh\nprintf "unselected compiler invoked\\n" >&2\nexit 71\n')
402531
competing.chmod(0o755)
532+
real_cxx = shlex.split(os.environ.get('CXX', 'c++'))
533+
real_cxx[0] = shutil.which(real_cxx[0]) or real_cxx[0]
403534
env = dict(os.environ, CXX=shlex.join([sys.executable, str(selected), '--selected-toolchain']),
404535
PATH=str(root) + os.pathsep + os.environ['PATH'],
405-
INVOCATIONS=str(root / 'calls.jsonl'), REAL_CXX=shutil.which('c++'))
536+
INVOCATIONS=str(root / 'calls.jsonl'), REAL_CXX=shlex.join(real_cxx))
406537
result = subprocess.run([sys.executable, str(ROOT / 'scripts/test-v3-installed-package.py'),
407538
'AuditTest.test_native_public_api_is_available_through_umbrella',
408539
'AuditTest.test_current_umbrella_has_no_backend_types'],

0 commit comments

Comments
 (0)