Skip to content
Merged
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
698 changes: 698 additions & 0 deletions .agents/docs/2026-10-19-first-run-home-and-network-design.md

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion .agents/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ superseded_by: 2026-09-07-....md # when status is superseded
---
```

333 records.
334 records.

## By subject

Expand Down Expand Up @@ -66,6 +66,10 @@ Records that declare one. Everything else is listed by date below.
- [The island boundary's names: one rule for both lanes, and the check that makes it true](2026-09-08-island-boundary-names.md) — active
- [Implementation plan: the island boundary's names](2026-09-08-island-boundary-names-implementation-plan.md) — active

### home

- [首次初始化、home 重定向与网络诊断优化设计](2026-10-19-first-run-home-and-network-design.md) — active

### modules

- [Two answers and two silences: the scanner's second grammar, and the manifest keys nothing reads](2026-09-09-two-answers-and-two-silences.md) — active
Expand Down Expand Up @@ -130,6 +134,7 @@ Records that declare one. Everything else is listed by date below.

### 2026-10

- [首次初始化、home 重定向与网络诊断优化设计](2026-10-19-first-run-home-and-network-design.md) — active
- [升级的连续性:打包修订不在使用路径上替换载荷、替换失败不拆掉可用状态、home 中的悬空记录自愈、真实的升级 lane](2026-10-17-upgrade-continuity-design.md) — active
- [mcpp 对 xlings 的依赖:Provider 端口、归属与位置、上游复用、系统级安装、payload 不可变、文件系统能力与长路径(#809)](2026-10-16-xlings-provider-port-and-shared-store-design.md) — active
- [构建依赖的一条管线:配置与产物两轴、包的实例、库接口、兼容系统与工作空间收尾(#797)](2026-10-11-build-dependencies-one-pipeline-design.md) — active
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/bootstrap-mcpp/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ inputs:
# `package.name`, so one of the two was simply unreachable — and which one
# depended on the machine, which is why CI failed on `compat:lua` on
# Windows and `mcpplibs.capi:lua` on Linux. Never pin below that.
default: '2026.10.17.1'
default: '2026.10.19.1'

outputs:
sandbox-key:
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/setup-macos-llvm/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ inputs:
# Floor imposed by the index, not a routine bump — see
# .github/actions/bootstrap-mcpp/action.yml for why 0.4.69 is required
# (two packages named `lua` in one repo need openxlings/xlings#381).
default: '2026.10.17.1'
default: '2026.10.19.1'
image:
description: >
The runner label the job runs on (macos-15, xcode-27). It is part of the
Expand Down
28 changes: 28 additions & 0 deletions .github/tools/check_windows_home_legacy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Prove locator round-trips under a genuinely non-UTF-8 Windows ACP."""
import os
from pathlib import Path
import re
import subprocess
import sys

if os.name != 'nt':
raise SystemExit('This check requires Windows')
root = Path(__file__).resolve().parents[2]
manifest = root / 'mcpp.toml'
original = manifest.read_bytes()
marker = b'windows_code_page = "utf-8"'
assert original.count(marker) == 1, 'identify the test-only code-page setting explicitly'
try:
manifest.write_bytes(original.replace(marker, b'windows_code_page = "legacy"', 1))
result = subprocess.run([os.environ['MCPP'], '--offline', 'test', 'home'], cwd=root,
capture_output=True, text=True, encoding='utf-8', errors='replace',
timeout=600)
sys.stdout.write(result.stdout)
sys.stderr.write(result.stderr)
assert result.returncode == 0, result.returncode
output = re.sub(r'\x1b\[[0-9;]*m', '', result.stdout + result.stderr)
assert ('[ OK ] HomeSelection.UnicodeLocatorRoundTripDoesNotNarrowTheSelectedPath'
in output), 'the legacy ACP case did not pass; a UTF-8-only skip is not proof'
finally:
manifest.write_bytes(original)
print('PASS: the locator round-trip ran under legacy Windows ACP; manifest restored')
223 changes: 223 additions & 0 deletions .github/tools/check_windows_home_menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
"""Drive the real first-run CLI through a native Windows console, not a pipe."""
import ctypes as c
from ctypes import wintypes as w
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import time

if os.name != 'nt':
raise SystemExit('This check requires Windows')

kernel = c.WinDLL('kernel32', use_last_error=True)
INVALID_HANDLE_VALUE = c.c_void_p(-1).value

class Coord(c.Structure):
_fields_ = [('X', c.c_short), ('Y', c.c_short)]

class Rect(c.Structure):
_fields_ = [(name, c.c_short) for name in ('Left', 'Top', 'Right', 'Bottom')]

class ScreenInfo(c.Structure):
_fields_ = [('size', Coord), ('cursor', Coord), ('attributes', w.WORD),
('window', Rect), ('maximum', Coord)]

class KeyChar(c.Union):
_fields_ = [('UnicodeChar', w.WCHAR), ('AsciiChar', c.c_char)]

class KeyEvent(c.Structure):
_fields_ = [('down', w.BOOL), ('repeat', w.WORD), ('key', w.WORD),
('scan', w.WORD), ('char', KeyChar), ('state', w.DWORD)]

class Event(c.Union):
_fields_ = [('key', KeyEvent), ('padding', c.c_byte * 16)]

class InputRecord(c.Structure):
_fields_ = [('type', w.WORD), ('event', Event)]

kernel.CreateFileW.argtypes = [w.LPCWSTR, w.DWORD, w.DWORD, c.c_void_p,
w.DWORD, w.DWORD, w.HANDLE]
kernel.CreateFileW.restype = w.HANDLE
kernel.AttachConsole.argtypes = [w.DWORD]
kernel.AttachConsole.restype = w.BOOL
kernel.GetConsoleScreenBufferInfo.argtypes = [w.HANDLE, c.POINTER(ScreenInfo)]
kernel.GetConsoleScreenBufferInfo.restype = w.BOOL
kernel.ReadConsoleOutputCharacterW.argtypes = [w.HANDLE, w.LPWSTR, w.DWORD,
Coord, c.POINTER(w.DWORD)]
kernel.ReadConsoleOutputCharacterW.restype = w.BOOL
kernel.WriteConsoleInputW.argtypes = [w.HANDLE, c.POINTER(InputRecord), w.DWORD,
c.POINTER(w.DWORD)]
kernel.WriteConsoleInputW.restype = w.BOOL
kernel.CloseHandle.argtypes = [w.HANDLE]
kernel.SetConsoleCtrlHandler.argtypes = [c.c_void_p, w.BOOL]
kernel.SetConsoleCtrlHandler.restype = w.BOOL
kernel.GenerateConsoleCtrlEvent.argtypes = [w.DWORD, w.DWORD]
kernel.GenerateConsoleCtrlEvent.restype = w.BOOL
kernel.GetConsoleCP.argtypes = []
kernel.GetConsoleCP.restype = w.UINT
kernel.AllocConsole.argtypes = []
kernel.AllocConsole.restype = w.BOOL

def opened(name):
handle = kernel.CreateFileW(name, 0xC0000000, 3, None, 3, 0, None)
if handle == INVALID_HANDLE_VALUE:
raise c.WinError(c.get_last_error())
return handle

def screen(handle):
info = ScreenInfo()
if not kernel.GetConsoleScreenBufferInfo(handle, c.byref(info)):
raise c.WinError(c.get_last_error())
width = info.size.X
rows = min(info.size.Y, max(60, info.cursor.Y + 3))
data = c.create_unicode_buffer(width * rows + 1)
count = w.DWORD()
if not kernel.ReadConsoleOutputCharacterW(handle, data, width * rows, Coord(0, 0), c.byref(count)):
raise c.WinError(c.get_last_error())
return '\n'.join(data.value[i:i + width].rstrip() for i in range(0, count.value, width))

def input_line(handle, value):
for char in value + '\r':
record = InputRecord()
record.type = 1
record.event.key.down = True
record.event.key.repeat = 1
record.event.key.key = 13 if char == '\r' else ord('Z') if char == '\x1a' else ord(char.upper())
record.event.key.char.UnicodeChar = char
record.event.key.state = 8 if char == '\x1a' else 0 # LEFT_CTRL_PRESSED for EOF
count = w.DWORD()
if not kernel.WriteConsoleInputW(handle, c.byref(record), 1, c.byref(count)):
raise c.WinError(c.get_last_error())
assert count.value == 1

def check_layout(text):
lines = text.splitlines()
choose = next(i for i, line in enumerate(lines) if 'Choose the mcpp home:' in line)
select = next(i for i, line in enumerate(lines) if 'Select [1/2, Enter=1]:' in line)
assert lines[choose].index('Choose') == lines[select].index('Select') == 6, text
assert not lines[choose + 1].strip(), text
assert '1) D:\\mcpp' in lines[choose + 2] and '[default]' in lines[choose + 2], text
assert '2) ' in lines[choose + 3], text
assert not lines[select - 1].strip(), text

source = Path(os.environ['MCPP']).resolve()
with tempfile.TemporaryDirectory(prefix='mcpp-console-menu-') as temporary:
root = Path(temporary)
# `target` deliberately excludes portable-home resolution. Each profile is fresh.
binary = root / 'target' / 'bin' / 'mcpp.exe'
binary.parent.mkdir(parents=True)
shutil.copy2(source, binary)

def run_case(name, choice):
profile = root / name
profile.mkdir()
env = os.environ.copy()
env.pop('MCPP_HOME', None)
env.update(USERPROFILE=str(profile), HOME=str(profile), MCPP_OFFLINE='1')
# 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), 'self', 'init'], env=env, cwd=root,
creationflags=subprocess.CREATE_NEW_CONSOLE)
output = stdin = None
ignored_interrupt = False
try:
if choice is None:
# A native console is still attached to the child. The former
# drive-only check waits for input here and hits this timeout.
code = proc.wait(timeout=10)
assert code in (0, 1), code
assert (profile / '.mcpp' / 'config.toml').is_file()
assert not (profile / '.mcpp' / 'home.toml').exists()
return
kernel.FreeConsole()
until = time.monotonic() + 10
while not kernel.AttachConsole(proc.pid):
if proc.poll() is not None or time.monotonic() >= until:
raise AssertionError('Could not attach the native child console')
time.sleep(.02)
output, stdin = opened('CONOUT$'), opened('CONIN$')
while True:
text = screen(output)
if 'Select [1/2, Enter=1]:' in text:
break
if proc.poll() is not None or time.monotonic() >= until:
raise AssertionError(text or 'No home menu appeared')
time.sleep(.02)
check_layout(text)
if name == 'interrupt':
# The controller shares the child's console; only it ignores
# this broadcast. The child was spawned before the ignore flag.
if not kernel.SetConsoleCtrlHandler(None, True):
raise c.WinError(c.get_last_error())
ignored_interrupt = True
if not kernel.GenerateConsoleCtrlEvent(0, 0): # CTRL_C_EVENT
raise c.WinError(c.get_last_error())
else:
input_line(stdin, choice)
code = proc.wait(timeout=20)
text = screen(output)
print(name, 'exit', code, '\n' + text, flush=True)
if name in ('invalid', 'eof', 'interrupt'):
if name == 'invalid':
assert code == 2 and '--home' in text, text
elif name == 'eof':
assert code == 1, (code, text)
else:
assert code in (1, -1073741510, 3221225786), (code, text)
assert not (profile / '.mcpp' / 'home.toml').exists()
assert not (profile / '.mcpp' / 'config.toml').exists()
else:
assert code in (0, 1), text # offline initialization can lack tools
data = subprocess.run([str(binary), 'self', 'env', '--format', 'json'],
env=env, cwd=root, capture_output=True, text=True,
encoding='utf-8', timeout=10, check=True)
home = Path(json.loads(data.stdout)['data']['mcppHome'])
expected = Path('D:/mcpp') if name in ('drive', 'default') else profile / '.mcpp'
assert os.path.samefile(home, expected), data.stdout
assert (expected / 'config.toml').is_file(), text
finally:
if proc.poll() is None:
proc.kill()
proc.wait(timeout=5)
for handle in (stdin, output):
if handle is not None:
kernel.CloseHandle(handle)
kernel.FreeConsole()
if ignored_interrupt:
kernel.SetConsoleCtrlHandler(None, False)

# This check runs on an ephemeral hosted Windows runner. Refuse an existing
# D:\mcpp rather than touching a home this test did not create.
default = Path('D:/mcpp')
assert Path('D:/').is_dir(), 'The native-menu lane requires its hosted D drive'
default.mkdir() # exclusive reservation; an existing home is a test setup error
marker = default / '.mcpp-console-fixture'
marker.write_text('owned by this isolated CI test', encoding='utf-8')
try:
run_case('invalid', '3')
run_case('eof', '\x1a')
run_case('interrupt', '')
run_case('user', '2')
run_case('drive', '1')
run_case('default', '')
shutil.rmtree(default) # this test exclusively reserved the directory
target = root / 'junction-target'
target.mkdir()
subprocess.run(['cmd', '/d', '/c', 'mklink', '/J', str(default), str(target)],
capture_output=True, text=True, check=True, timeout=5)
try:
run_case('reparse', None)
assert not list(target.iterdir()), 'the suggestion followed a reparse point'
finally:
default.rmdir() # remove the junction, retaining its target
finally:
if marker.is_file():
shutil.rmtree(default)
print('PASS: native Windows menu layout, input, cancellation, default, and reparse exclusion')
2 changes: 1 addition & 1 deletion .github/workflows/bootstrap-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
# Dormant (workflow_dispatch only), but kept in step with the rest —
# check_version_pins.sh holds it there. Floor: 0.4.69, below which the
# index cannot resolve two packages that share a short name.
XLINGS_VERSION: '2026.10.17.1'
XLINGS_VERSION: '2026.10.19.1'
steps:
- uses: actions/checkout@v4

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci-bootstrap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
os: [ubuntu-24.04, macos-15, windows-2025]
env:
XLINGS_NON_INTERACTIVE: '1'
XLINGS_VERSION: '2026.10.17.1'
XLINGS_VERSION: '2026.10.19.1'
steps:
- uses: actions/checkout@v4

Expand All @@ -50,7 +50,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.17.1
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.19.1
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
echo "$HOME/.xlings/bin" >> "$GITHUB_PATH"

Expand Down Expand Up @@ -124,7 +124,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.17.1
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.19.1
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Install the newest release and build the tree with it (mcpp build --strict)
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci-fresh-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ jobs:
env:
XLINGS_NON_INTERACTIVE: '1'
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.17.1
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.19.1
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Install mcpp and config mirror
Expand Down Expand Up @@ -315,7 +315,7 @@ jobs:

- name: Install xlings + mcpp
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.17.1
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.19.1
# Deliberately NOT writing to $GITHUB_PATH here. On container
# images that declare no PATH in their config (opensuse/
# tumbleweed), appending a single dir to GITHUB_PATH makes the
Expand Down Expand Up @@ -464,7 +464,7 @@ jobs:
# (older ones carry minos=15 and refuse to start).
# v0.4.51+: in-process sha256 — this image has no sha256sum
# binary, so pinned fetches failed before it.
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.17.1
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.19.1
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"

- name: Install mcpp and config mirror
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci-linux-e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ jobs:

- name: Bootstrap xlings + released mcpp
run: |
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.17.1
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.19.1
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
xlings update
xlings install mcpp -y -g
Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ci-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,22 @@ jobs:
"$MCPP_FRESH" --version
echo "MCPP_SELF=$MCPP_FRESH" >> "$GITHUB_ENV"

- name: The first home menu is aligned in a native Windows console
shell: bash
run: MCPP="$MCPP_SELF" python .github/tools/check_windows_home_menu.py

- name: Unit + integration tests via mcpp test
shell: bash
run: |
export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe")
"$MCPP_SELF" test

- name: Home locators preserve Unicode under a legacy Windows code page
shell: bash
run: |
export MCPP_VENDORED_XLINGS=$(cygpath -w "$USERPROFILE/.xlings/subos/default/bin/xlings.exe")
MCPP="$MCPP_SELF" python .github/tools/check_windows_home_legacy.py

# WS8: this row has Visual Studio, so its answer is the "Windows with
# usable MSVC" row of docs/01 and docs/20.
- name: The documented default toolchain is this host's answer
Expand Down
Loading
Loading