Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/probe-cold-emit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: cold emit Windows probe
on:
pull_request:
paths:
- '.github/workflows/probe-cold-emit.yml'
permissions:
contents: read
actions: read
jobs:
cold-emit:
runs-on: windows-2025
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
ref: 04c2aafff599cb650258ddac195a3eb663abbf11
- uses: ./.github/actions/bootstrap-mcpp
- uses: actions/download-artifact@v4
with:
name: mcpp-built-windows-x86_64
path: ${{ runner.temp }}/mcpp-built
run-id: 39192774351
github-token: ${{ github.token }}
- name: Use the feature candidate
shell: bash
run: bash .github/actions/use-built-mcpp/use.sh windows-x86_64 GLOBAL
- name: Reproduce the cold planning failure with its JSON diagnostics
shell: bash
run: |
"$MCPP" toolchain default llvm@23.1.3
python3 - <<'PY'
from pathlib import Path
p = Path('tests/e2e/688_emit_build_database.sh')
s = p.read_text().replace('|| fail "L: emit in a home with a cold std cache" "$OUT/cold.err"', '|| fail "L: emit in a home with a cold std cache" "$OUT/cold.err" "$OUT/cold.json"')
p.write_text(s)
PY
bash tests/e2e/688_emit_build_database.sh 2>&1 | tee "$RUNNER_TEMP/cold-emit.log"
- uses: actions/upload-artifact@v4
if: always()
with:
name: cold-emit-windows
path: ${{ runner.temp }}/cold-emit.log
retention-days: 7
95 changes: 95 additions & 0 deletions .github/workflows/probe-first-run.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: first-run design probes
on:
pull_request:
paths:
- 'tests/probes/first_run_home_network.py'
- 'tests/probes/transitive_module.py'
- '.github/workflows/probe-first-run.yml'
permissions:
contents: read
jobs:
baseline:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
asset: linux-x86_64.tar.gz
- os: windows-2025
asset: windows-x86_64.zip
- os: macos-15
asset: macosx-arm64.tar.gz
runs-on: ${{ matrix.os }}
timeout-minutes: 12
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Fetch the pinned released baseline
shell: bash
env:
GH_TOKEN: ${{ github.token }}
ASSET: mcpp-2026.10.17.1-${{ matrix.asset }}
run: |
mkdir -p "$RUNNER_TEMP/release-download" "$RUNNER_TEMP/release-probe"
gh release download v2026.10.17.1 --repo mcpp-community/mcpp --pattern "$ASSET" --pattern "$ASSET.sha256" --dir "$RUNNER_TEMP/release-download"
python - <<'PY'
import hashlib, os, pathlib, shutil
root = pathlib.Path(os.environ['RUNNER_TEMP'])
archive = root / 'release-download' / os.environ['ASSET']
expected = archive.with_name(archive.name + '.sha256').read_text().split()[0]
assert hashlib.sha256(archive.read_bytes()).hexdigest() == expected
shutil.unpack_archive(str(archive), str(root / 'release-probe'))
PY
- name: Observe home, offline diagnostics and refused proxy behavior
shell: bash
run: |
release=$(find "$RUNNER_TEMP/release-probe" -mindepth 1 -maxdepth 1 -type d | head -1)
python tests/probes/first_run_home_network.py --release "$release" --out probe-results
- uses: actions/upload-artifact@v4
if: always()
with:
name: first-run-${{ matrix.os }}
path: probe-results/
retention-days: 7
module-control:
runs-on: macos-15
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-macos-llvm
with:
image: macos-15
- name: Select a real pinned mcpp binary rather than a workspace shim
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p "$RUNNER_TEMP/module-release"
gh release download v2026.10.17.1 --repo mcpp-community/mcpp --pattern mcpp-2026.10.17.1-macosx-arm64.tar.gz --pattern mcpp-2026.10.17.1-macosx-arm64.tar.gz.sha256 --dir "$RUNNER_TEMP/module-release"
python3 - <<'PY'
import hashlib, os, pathlib, shutil
root = pathlib.Path(os.environ['RUNNER_TEMP']) / 'module-release'
archive = root / 'mcpp-2026.10.17.1-macosx-arm64.tar.gz'
assert hashlib.sha256(archive.read_bytes()).hexdigest() == archive.with_name(archive.name + '.sha256').read_text().split()[0]
shutil.unpack_archive(str(archive), str(root))
PY
echo "MCPP=$RUNNER_TEMP/module-release/mcpp-2026.10.17.1-macosx-arm64/bin/mcpp" >> "$GITHUB_ENV"
echo "MCPP_TOOLCHAIN=llvm@$MCPP_LLVM_VER" >> "$GITHUB_ENV"
- name: Prepare the existing sandbox before offline build probes
timeout-minutes: 4
shell: bash
run: |
export MCPP_HOME="$HOME/.mcpp"
"$MCPP" self init
- name: Compare direct, transitive and header-discovered imports
timeout-minutes: 6
shell: bash
run: python3 tests/probes/transitive_module.py
- uses: actions/upload-artifact@v4
if: always()
with:
name: module-control-macos-15
path: module-probe-results/
retention-days: 7
41 changes: 41 additions & 0 deletions .github/workflows/probe-home-console.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: native home cancellation probe
on:
pull_request:
paths:
- '.github/workflows/probe-home-console.yml'
permissions:
contents: read
actions: read
jobs:
console:
runs-on: windows-2025
timeout-minutes: 8
steps:
- uses: actions/checkout@v4
with:
ref: be6fb71144c448b9c479bf867b2ee1cad95fb2f6
- uses: actions/download-artifact@v4
with:
name: mcpp-built-windows-x86_64
path: ${{ runner.temp }}/mcpp-built
run-id: 39213380201
github-token: ${{ github.token }}
- name: Reset the launcher's inherited Ctrl-C attribute before child creation
shell: bash
run: |
python - <<'PY'
from pathlib import Path
p = Path('.github/tools/check_windows_home_menu.py')
s = p.read_text()
needle = ' proc = subprocess.Popen([str(binary),'
assert s.count(needle) == 1
s = s.replace(needle, ''' # A CI shell can leave its descendants ignoring Ctrl-C. Give this
# native-console fixture an ordinary interactive launch context.
if kernel.GetConsoleCP() == 0 and not kernel.AllocConsole():
raise c.WinError(c.get_last_error())
if not kernel.SetConsoleCtrlHandler(None, False):
raise c.WinError(c.get_last_error())
proc = subprocess.Popen([str(binary),''')
p.write_text(s)
PY
MCPP="$(cygpath -w "$RUNNER_TEMP/mcpp-built/mcpp.exe")" python .github/tools/check_windows_home_menu.py
140 changes: 140 additions & 0 deletions tests/probes/first_run_home_network.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Observe released mcpp behavior; no proposed home/network implementation.

Each child has a deadline and is killed as a tree. Proxy refusal is an injected
failure, not a simulation of every DNS/TLS/packet-loss failure.
"""
import argparse
import ctypes
import json
import os
from pathlib import Path
import platform
import shutil
import signal
import subprocess
import tempfile
import time

p = argparse.ArgumentParser()
p.add_argument('--release', required=True)
p.add_argument('--out', required=True)
a = p.parse_args()
release = Path(a.release).resolve()
out = Path(a.out).resolve()
out.mkdir(parents=True, exist_ok=True)
scratch = Path(tempfile.mkdtemp(prefix='mcpp-first-run-'))
exe = 'mcpp.exe' if os.name == 'nt' else 'mcpp'
binary = release / 'bin' / exe
assert binary.is_file(), binary
provider = next(release.rglob('xlings.exe' if os.name == 'nt' else 'xlings'))
env = os.environ.copy()
for k in ('MCPP_HOME', 'MCPP_OFFLINE', 'MCPP_SYSTEM_CONFIG'):
env.pop(k, None)
env.update(HOME=str(scratch / 'user'), USERPROFILE=str(scratch / 'user'),
MCPP_VENDORED_XLINGS=str(provider), XLINGS_NON_INTERACTIVE='1')
(scratch / 'user').mkdir()
rows = []

def run(name, args, changes=None, executable=None, limit=20):
childenv = env | (changes or {})
cmd = [str(executable or binary), *args]
start = time.monotonic()
log = out / (name + '.log')
timed_out = False
with log.open('wb') as f:
proc = subprocess.Popen(cmd, env=childenv, cwd=scratch, stdin=subprocess.DEVNULL,
stdout=f, stderr=subprocess.STDOUT,
start_new_session=os.name != 'nt')
try:
rc = proc.wait(timeout=limit)
except subprocess.TimeoutExpired:
timed_out = True
if os.name == 'nt':
subprocess.run(['taskkill', '/PID', str(proc.pid), '/T', '/F'],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=10)
else:
os.killpg(proc.pid, signal.SIGKILL)
proc.wait(timeout=10)
rc = proc.returncode
text = log.read_text(encoding='utf-8', errors='replace')
row = dict(name=name, command=args, rc=rc, timed_out=timed_out,
seconds=round(time.monotonic() - start, 3))
rows.append(row)
print(json.dumps(row), flush=True)
return text, row

def home_of(text):
data = json.loads(text)
return Path(data.get('data', data)['mcppHome'])

try:
run('version', ['--version'])
explicit = scratch / 'explicit'
text, row = run('explicit-json', ['--offline', 'self', 'env', '--format', 'json'],
{'MCPP_HOME': str(explicit)})
assert row['rc'] == 0 and home_of(text) == explicit
assert not explicit.exists(), 'JSON env unexpectedly initialized the home'

text, row = run('portable-json', ['--offline', 'self', 'env', '--format', 'json'])
assert row['rc'] == 0 and home_of(text) == release

# The dev layout keeps the same executable-relative runtime layout while
# excluding self-contained mode through the existing target-ancestor rule.
devroot = scratch / 'target' / 'probe'
shutil.copytree(release, devroot)
devbin = devroot / 'bin' / exe
text, row = run('fallback-json', ['--offline', 'self', 'env', '--format', 'json'],
executable=devbin)
assert row['rc'] == 0 and home_of(text) == scratch / 'user' / '.mcpp'

offline_home = scratch / 'offline'
text, row = run('offline-doctor', ['--offline', 'self', 'doctor'],
{'MCPP_HOME': str(offline_home)})
assert not row['timed_out']
assert not (offline_home / 'registry' / 'data' / 'xim-pkgindex').exists()
assert not (offline_home / 'registry' / 'subos' / 'default' / '.xlings.json').exists()
assert 'Checking build policy' in text, 'doctor did not reach later diagnostics'

force_home = scratch / 'force'
removed = ('registry', 'cache', 'build-cache', 'bmi')
for name in (*removed, 'keep'):
d = force_home / name
d.mkdir(parents=True)
(d / 'probe-sentinel').write_text('retain?', encoding='utf-8')
text, row = run('force-offline', ['--offline', 'self', 'init', '--force'],
{'MCPP_HOME': str(force_home)})
row['removed_sentinels'] = [n for n in removed if not (force_home / n / 'probe-sentinel').exists()]
row['unrelated_retained'] = (force_home / 'keep' / 'probe-sentinel').exists()
assert len(row['removed_sentinels']) == 4 and row['unrelated_retained']

# Inject a closed proxy port; output is evidence, not a green assertion that
# ordinary doctor is bounded (the baseline may exceed this observation).
proxy = dict(HTTPS_PROXY='http://127.0.0.1:9', HTTP_PROXY='http://127.0.0.1:9',
ALL_PROXY='http://127.0.0.1:9', https_proxy='http://127.0.0.1:9',
http_proxy='http://127.0.0.1:9', all_proxy='http://127.0.0.1:9',
NO_PROXY='', no_proxy='')
for name, args in (
('blocked-doctor', ['self', 'doctor']),
('blocked-config', ['self', 'config', '--mirror', 'GLOBAL']),
):
h = scratch / name
text, row = run(name, args, proxy | {'MCPP_HOME': str(h)}, limit=12)
row['started_bootstrap'] = any(s in text for s in ('Initialize', 'Bootstrap', 'Fetching'))
cfg = h / 'registry' / '.xlings.json'
row['seeded_mirror'] = json.loads(cfg.read_text()).get('mirror') if cfg.exists() else None

inventory = dict(os=platform.platform(), python=platform.python_version())
if os.name == 'nt':
drive_type = ctypes.windll.kernel32.GetDriveTypeW('D:\\')
inventory['d_drive_type'] = drive_type
inventory['d_writable'] = False
if drive_type == 3:
try:
with tempfile.TemporaryDirectory(prefix='mcpp-probe-', dir='D:\\'):
inventory['d_writable'] = True
except OSError as e:
inventory['d_write_error'] = type(e).__name__
(out / 'inventory.json').write_text(json.dumps(inventory, indent=2), encoding='utf-8')
finally:
(out / 'results.json').write_text(json.dumps(rows, indent=2), encoding='utf-8')
shutil.rmtree(scratch, ignore_errors=True)
67 changes: 67 additions & 0 deletions tests/probes/transitive_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""Observe #816 with a local dependency, including direct-import controls."""
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import time

out = Path('module-probe-results').resolve()
out.mkdir(exist_ok=True)
root = Path(tempfile.mkdtemp(prefix='mcpp-module-probe-'))
env = os.environ.copy()
env['MCPP_OFFLINE'] = '1'
env['MCPP_HOME'] = str(Path.home() / '.mcpp')
rows = []
try:
dep = root / 'dep'
(dep / 'src').mkdir(parents=True)
(dep / 'mcpp.toml').write_text('''[package]
name = "dep"
version = "0.1.0"
[language]
standard = "c++23"
import_std = false
[targets.dep]
kind = "lib"
''')
(dep / 'src' / 'dep.cppm').write_text('export module depmod;\nexport int answer() { return 42; }\n')
for mode in ('direct', 'transitive', 'header-transitive'):
app = root / mode
(app / 'src').mkdir(parents=True)
(app / 'mcpp.toml').write_text('''[package]
name = "probe"
version = "0.1.0"
[language]
standard = "c++23"
import_std = false
[dependencies]
dep = { path = "../dep" }
''')
if mode == 'direct':
source = 'import depmod;\nint main() { return answer() == 42 ? 0 : 1; }\n'
else:
(app / 'src' / 'mylib.cppm').write_text('export module mylib;\nimport depmod;\nexport int value() { return answer(); }\n')
source = 'import mylib;\nint main() { return value() == 42 ? 0 : 1; }\n'
if mode == 'header-transitive':
(app / 'src' / 'helper.h').write_text('#pragma once\nimport mylib;\n')
source = '#include "helper.h"\nint main() { return value() == 42 ? 0 : 1; }\n'
(app / 'src' / 'main.cpp').write_text(source)
start = time.monotonic()
# The Actions step supplies an outer timeout as well.
result = subprocess.run([env['MCPP'], 'build'], cwd=app, env=env,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=90)
(out / (mode + '.log')).write_bytes(result.stdout)
row = dict(mode=mode, rc=result.returncode, seconds=round(time.monotonic()-start, 3))
rows.append(row)
print(json.dumps(row), flush=True)
db = app / 'compile_commands.json'
if db.exists():
shutil.copyfile(db, out / (mode + '-compile_commands.json'))
for n, ninja in enumerate(app.rglob('build.ninja')):
shutil.copyfile(ninja, out / (mode + f'-{n}-build.ninja'))
assert rows[0]['rc'] == 0, 'direct-import control failed; cannot interpret transitive results'
finally:
(out / 'results.json').write_text(json.dumps(rows, indent=2))
shutil.rmtree(root, ignore_errors=True)
Loading