diff --git a/.github/workflows/fresh-install.yml b/.github/workflows/fresh-install.yml
index 5b7a90f..cedd1c8 100644
--- a/.github/workflows/fresh-install.yml
+++ b/.github/workflows/fresh-install.yml
@@ -218,14 +218,14 @@ jobs:
include-hidden-files: true
released-course:
- name: Published d2x install route (main only)
- if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && inputs.candidate_run == ''
+ name: d2x download and pinned install
+ if: ${{ !inputs.diagnose && inputs.candidate_run == '' }}
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- - name: Follow the README using the public course download
- run: python3 scripts/ci/readme.py --published
+ - name: Verify the documented existing-d2x download route
+ run: python3 scripts/ci/readme.py --d2x-download ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' && '--published' || '' }}
- uses: actions/upload-artifact@v4
if: always()
with:
@@ -265,7 +265,7 @@ jobs:
include-hidden-files: true
latest-stable:
- if: github.ref == 'refs/heads/main' && inputs.candidate_run == ''
+ if: ${{ !inputs.diagnose && inputs.candidate_run == '' }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
@@ -280,7 +280,7 @@ jobs:
if: runner.os == 'Windows'
shell: powershell
run: ./scripts/install-xlings.ps1
- - name: Check stable release, public latest and course pin
+ - name: Check public latest and independently restore course pins
shell: bash
env:
GH_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/online-ebook.yml b/.github/workflows/online-ebook.yml
index 4f80503..aa40354 100644
--- a/.github/workflows/online-ebook.yml
+++ b/.github/workflows/online-ebook.yml
@@ -45,9 +45,6 @@ jobs:
run:
working-directory: book
- env:
- MDBOOK_VERSION: 0.4.43
-
steps:
- uses: actions/checkout@v4
@@ -57,8 +54,8 @@ jobs:
- name: Install the shared xlings baseline
run: bash ../scripts/install-xlings.sh
- - name: Install mdBook
- run: xlings install mdbook@${MDBOOK_VERSION} -y
+ - name: Install the course dependencies from .xlings.json
+ run: xlings install -y
- name: Setup Pages
if: github.event_name != 'pull_request'
diff --git a/README.md b/README.md
index 9849577..fef5434 100644
--- a/README.md
+++ b/README.md
@@ -43,22 +43,24 @@ Wait for the environment to be ready, then run `d2x checker` in the terminal.
### Practice locally
-**1. Install the tools**
+**1. Get the course**
-Install [xlings](docs/environment.md#install-xlings), reopen your terminal, then run:
+Install [xlings](docs/environment.md#install-xlings) and Git, then reopen your terminal.
```bash
-xlings update
-xlings install --use d2x@latest mcpp@latest -y
+git clone https://github.com/mcpp-community/d2mcpp.git
+cd d2mcpp
```
-**2. Get the course**
+**2. Install the course versions**
```bash
-d2x install d2mcpp
-cd d2mcpp
+xlings update
+xlings install -y
```
+Run these commands from the course directory. `xlings install` installs the versions declared in `.xlings.json`; after updating the course with Git, run the same two commands again.
+
**3. Verify and practice**
```bash
diff --git a/README.zh.hant.md b/README.zh.hant.md
index d9ff7cd..a5b5168 100644
--- a/README.zh.hant.md
+++ b/README.zh.hant.md
@@ -43,22 +43,24 @@
### 建立本地練習環境
-**第一步:安裝工具**
+**第一步:取得課程**
-安裝 [xlings](docs/environment.zh.md#install-xlings),重新開啟終端後執行:
+安裝 [xlings](docs/environment.zh.md#install-xlings) 和 Git,然後重新開啟終端。
```bash
-xlings update
-xlings install --use d2x@latest mcpp@latest -y
+git clone https://github.com/mcpp-community/d2mcpp.git
+cd d2mcpp
```
-**第二步:取得課程**
+**第二步:安裝課程版本**
```bash
-d2x install d2mcpp
-cd d2mcpp
+xlings update
+xlings install -y
```
+在課程目錄執行上述命令。`xlings install` 安裝 `.xlings.json` 宣告的版本;透過 Git 更新課程後,再次執行這兩條命令。
+
**第三步:驗證並開始練習**
```bash
diff --git a/README.zh.md b/README.zh.md
index 0d104b6..5e852bb 100644
--- a/README.zh.md
+++ b/README.zh.md
@@ -43,22 +43,24 @@
### 搭建本地练习环境
-**第一步:安装工具**
+**第一步:获取课程**
-安装 [xlings](docs/environment.zh.md#install-xlings),重新打开终端后执行:
+安装 [xlings](docs/environment.zh.md#install-xlings) 和 Git,然后重新打开终端。
```bash
-xlings update
-xlings install --use d2x@latest mcpp@latest -y
+git clone https://github.com/mcpp-community/d2mcpp.git
+cd d2mcpp
```
-**第二步:获取课程**
+**第二步:安装课程版本**
```bash
-d2x install d2mcpp
-cd d2mcpp
+xlings update
+xlings install -y
```
+在课程目录执行上述命令。`xlings install` 安装 `.xlings.json` 声明的版本;通过 Git 更新课程后,再次执行这两条命令。
+
**第三步:验证并开始练习**
```bash
diff --git a/book/en/src/base/chapter_1.md b/book/en/src/base/chapter_1.md
index caf0040..f02c3c7 100644
--- a/book/en/src/base/chapter_1.md
+++ b/book/en/src/base/chapter_1.md
@@ -40,32 +40,18 @@ $env:Path = "$env:USERPROFILE\.xlings\subos\current\bin;$env:Path"
The PATH command makes the installed tools available in this terminal. For future sessions, reopen your terminal after installation. Preparation can be repeated and preserves your exercise files and progress.
-Alternative: download with d2x install
+Alternative: download with an existing d2x
-Install [xlings](https://github.com/openxlings/xlings) first:
+If d2x is already installed, use the commands below. After downloading, run `xlings install -y` from the course directory to install the `.xlings.json` dependencies. There is no need to preinstall mcpp or select latest.
```bash
-curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/v2026.10.18.1/tools/other/quick_install.sh | bash -s v2026.10.18.1
-export PATH="$HOME/.xlings/subos/current/bin:$PATH"
-```
-
-Or on Windows PowerShell:
-
-```powershell
-irm https://raw.githubusercontent.com/mcpp-community/d2mcpp/main/scripts/install-xlings.ps1 | iex
-$env:Path = "$env:USERPROFILE\.xlings\subos\current\bin;$env:Path"
-```
-
-Then, on either system:
-
-```text
-xlings update
-xlings install --use d2x@latest mcpp@latest -y
d2x install d2mcpp
cd d2mcpp
+xlings update
+xlings install -y
```
-Continue with the preparation script for your system in step 2; both download routes use the same verification below.
+Then continue with the preparation script in step 2. Apply project pins again after updating the course.
diff --git a/book/en/src/changelog.md b/book/en/src/changelog.md
index 847d4bb..23467bc 100644
--- a/book/en/src/changelog.md
+++ b/book/en/src/changelog.md
@@ -10,6 +10,13 @@
## 2026/10
+**Install dependencies from the course manifest**
+
+Get the course before running `xlings install -y` to install the versions pinned in `.xlings.json`. Align update instructions and verify both Git and d2x downloads in CI. Check upstream latest independently of course dependency upgrades.
+
+- `Book`: [zh](https://mcpp-community.github.io/d2mcpp/base/chapter_1.html) / [en](https://mcpp-community.github.io/d2mcpp/en/base/chapter_1.html) - 2026/10/21
+- `Code`: [first-use CI](https://github.com/mcpp-community/d2mcpp/blob/main/.github/workflows/fresh-install.yml) - 2026/10/21
+
**d2x upgrade and cold-start diagnostics**
Upgrade d2x to 2026.10.21.1 and verify the selected executable; bound first Provider startup separately, show preparation and retain raw logs.
diff --git a/book/src/base/chapter_1.md b/book/src/base/chapter_1.md
index 8cb37f0..e27412c 100644
--- a/book/src/base/chapter_1.md
+++ b/book/src/base/chapter_1.md
@@ -40,32 +40,18 @@ $env:Path = "$env:USERPROFILE\.xlings\subos\current\bin;$env:Path"
PATH 命令让当前终端立即找到工具;安装后重新打开终端即可供后续会话使用。准备脚本可重复执行,不会改动练习文件或学习进度。
-可选:通过 d2x install 下载课程
+可选:通过已有 d2x 下载课程
-先安装 [xlings](https://github.com/openxlings/xlings):
+如果已经安装 d2x,可以使用以下方式。下载后在课程目录执行 `xlings install -y`,按 `.xlings.json` 安装依赖,无需提前安装 mcpp 或选择 latest。
```bash
-curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/v2026.10.18.1/tools/other/quick_install.sh | bash -s v2026.10.18.1
-export PATH="$HOME/.xlings/subos/current/bin:$PATH"
-```
-
-Windows PowerShell 使用:
-
-```powershell
-irm https://raw.githubusercontent.com/mcpp-community/d2mcpp/main/scripts/install-xlings.ps1 | iex
-$env:Path = "$env:USERPROFILE\.xlings\subos\current\bin;$env:Path"
-```
-
-然后在任一系统执行:
-
-```text
-xlings update
-xlings install --use d2x@latest mcpp@latest -y
d2x install d2mcpp
cd d2mcpp
+xlings update
+xlings install -y
```
-继续执行第二步中对应系统的准备脚本;两种下载方式共用下面的验证步骤。
+之后继续第二步的准备脚本。更新课程后同样按项目配置安装依赖。
diff --git a/book/src/changelog.md b/book/src/changelog.md
index 3d2977d..2ae3dd4 100644
--- a/book/src/changelog.md
+++ b/book/src/changelog.md
@@ -10,6 +10,13 @@
## 2026/10
+**按课程配置安装依赖**
+
+默认先获取课程,再由 `xlings install -y` 安装 `.xlings.json` 固定的版本;统一更新说明,并在 CI 验证 Git 与 d2x 下载路径。上游 latest 检查独立于课程版本升级。
+
+- `Book`: [zh](https://mcpp-community.github.io/d2mcpp/base/chapter_1.html) / [en](https://mcpp-community.github.io/d2mcpp/en/base/chapter_1.html) - 2026/10/21
+- `Code`: [first-use CI](https://github.com/mcpp-community/d2mcpp/blob/main/.github/workflows/fresh-install.yml) - 2026/10/21
+
**d2x 最新版联动与冷启动诊断**
升级 d2x 至 2026.10.21.1,核对实际执行版本;首次 Provider 构建单独限时,显示准备进度并保留原始日志。
diff --git a/docs/environment.md b/docs/environment.md
index ff8c5ca..0afde33 100644
--- a/docs/environment.md
+++ b/docs/environment.md
@@ -26,7 +26,7 @@ Continue with the README. If a command is not found, reopen your terminal or app
## Git installation and updates
-After installing xlings, you can also clone the course. Replace the URL with your fork if needed:
+After installing xlings, clone the course as shown in the README. Replace the URL with your fork if needed:
```bash
git clone https://github.com/mcpp-community/d2mcpp.git
@@ -104,6 +104,22 @@ limit. Preparation messages do not extend the startup deadline. On failure, keep
`.d2x/provider.stdout.log`, `.d2x/provider.stderr.log` and the command output.
Startup/transport errors exit nonzero; an unfinished exercise verdict is expected.
-For an existing tool installation, use `xlings install --use d2x@latest mcpp@latest -y` after `xlings update`. `@latest` selects the newest stable index entry; `--use` activates it even when already installed. Bare install commands can retain the previously selected version.
+From an existing course directory, install or restore the versions declared in `.xlings.json` with the commands below. `xlings update` refreshes the package index; `xlings install -y` installs the project dependencies. Use the course pins rather than `@latest`. Dependency upgrades belong in `.xlings.json` and must pass CI before publication.
+
+```bash
+xlings update
+xlings install -y
+```
User cancellation returns 130 and a `cancelled` event without a Provider-failure warning or a failed exercise verdict.
+
+## Download with an existing d2x
+
+If d2x is already available, you can use this download route. The installer applies the downloaded course dependencies; running `xlings install -y` from the course directory explicitly reapplies its pins. There is no need to preinstall mcpp or select latest.
+
+```bash
+d2x install d2mcpp
+cd d2mcpp
+xlings update
+xlings install -y
+```
diff --git a/docs/environment.zh.md b/docs/environment.zh.md
index d70c175..e5e8bfa 100644
--- a/docs/environment.zh.md
+++ b/docs/environment.zh.md
@@ -100,6 +100,22 @@ checker 在第一题之前显示准备信息。首次 Provider 启动的总期
准备提示不会延长期限。失败时保留 `.d2x/provider.stdout.log`、`.d2x/provider.stderr.log` 与命令输出。
启动和传输错误会非零退出;未完成练习的 fail 判定属于预期行为。
-已有工具环境先执行 `xlings update`,再执行 `xlings install --use d2x@latest mcpp@latest -y`。`@latest` 选择最新稳定条目;`--use` 即使新版已安装也会激活它。仅执行不带版本或激活选项的 install 可能继续保留旧版。
+在已有课程目录中,执行以下命令安装或恢复 `.xlings.json` 声明的版本。`xlings update` 刷新包索引;`xlings install -y` 按项目配置安装依赖。不要用 `@latest` 替代课程版本。升级依赖应先修改 `.xlings.json`,通过 CI 后发布。
+
+```bash
+xlings update
+xlings install -y
+```
用户取消返回 130 和 `cancelled` 事件,不报告 Provider 故障,也不生成练习失败判定。
+
+## 通过已有 d2x 下载课程
+
+如果已经安装 d2x,可以使用以下下载方式。安装器会应用下载课程中的依赖配置;进入课程目录后再次运行 `xlings install -y` 可明确核对安装。无需提前安装 mcpp 或切换到 latest。
+
+```bash
+d2x install d2mcpp
+cd d2mcpp
+xlings update
+xlings install -y
+```
diff --git a/scripts/ci/latest.py b/scripts/ci/latest.py
index d967524..d04868b 100644
--- a/scripts/ci/latest.py
+++ b/scripts/ci/latest.py
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
-"""Compare the stable d2x release, course pin and real public latest install."""
+"""Verify public latest independently, then verify the reviewed project pins."""
import json
import os
from pathlib import Path
@@ -7,6 +7,8 @@
import tempfile
import urllib.request
+from versions import verify
+
root = Path(__file__).resolve().parents[2]
logs = root / '.d2x/ci-logs'
logs.mkdir(parents=True, exist_ok=True)
@@ -25,8 +27,24 @@
assert old == '2026.08.02.2', 'Old-version activation fixture failed'
subprocess.run(['xlings', 'install', '--use', 'd2x@latest', '-y'], cwd=neutral, check=True)
actual = subprocess.check_output(['d2x', '--version'], cwd=neutral, text=True).strip()
-report = {'previous': old, 'latest': latest, 'course_pin': pin, 'public_install': actual, 'release': release['html_url']}
+ # Deliberately keep a project behind latest to exercise version isolation,
+ # even when the real course pin currently equals the latest release.
+ project = Path(neutral) / 'older-course'
+ project.mkdir()
+ fixture = json.dumps({'workspace': {'d2x': old}}) + '\n'
+ (project / '.xlings.json').write_text(fixture)
+ subprocess.run(['xlings', 'install', '-y'], cwd=project, check=True)
+ selected = subprocess.check_output(['d2x', '--version'], cwd=project, text=True).strip()
+ assert selected == old, f'Project pin ignored: {selected}; expected {old}'
+ assert (project / '.xlings.json').read_text() == fixture, 'Install rewrote the fixture pin'
+ outside = subprocess.check_output(['d2x', '--version'], cwd=neutral, text=True).strip()
+ assert outside == actual, 'Project install changed the global selection'
+report = {'previous': old, 'latest': latest, 'course_pin': pin, 'public_install': actual, 'release': release['html_url'], 'older_project_selection': selected, 'global_after_project_install': outside}
(logs / 'latest.json').write_text(json.dumps(report, indent=2))
print(json.dumps(report, indent=2), flush=True)
assert actual == latest, 'Public index/install is behind the stable release'
-assert pin == latest, 'Bump the course pin and run the full first-use matrix for the new stable release'
+# A new upstream release must not force an unreviewed course dependency bump.
+manifest = (root / '.xlings.json').read_bytes()
+subprocess.run(['xlings', 'install', '-y'], cwd=root, check=True)
+verify(label='pinned-after-latest')
+assert (root / '.xlings.json').read_bytes() == manifest, 'Install changed the reviewed manifest'
diff --git a/scripts/ci/readme.py b/scripts/ci/readme.py
index 4a1b4c4..6d26a1e 100644
--- a/scripts/ci/readme.py
+++ b/scripts/ci/readme.py
@@ -43,8 +43,23 @@ def readme_commands(name, start, end):
install = re.search(r'\s*##[^\n]*\n(.*?)(?=\n## |\Z)', guide, re.S)[1]
bootstrap = code_blocks(install, 'powershell' if os.name == 'nt' else 'bash')[0]
parser = argparse.ArgumentParser(description=__doc__)
+parser.add_argument('--d2x-download', action='store_true', help='verify the optional existing-d2x download route')
parser.add_argument('--published', action='store_true', help='download the public course instead of the candidate fixture')
args = parser.parse_args()
+if args.d2x_download:
+ # Only this optional route needs d2x before the course exists. Bootstrap the
+ # reviewed course version in a neutral directory; mcpp comes from its manifest.
+ pins = json.loads((ROOT / '.xlings.json').read_text())['workspace']
+ commands = code_blocks(guide.split('## Download with an existing d2x', 1)[1], 'bash')[0].splitlines()
+ assert commands == code_blocks(translation.split('## 通过已有 d2x 下载课程', 1)[1], 'bash')[0].splitlines()
+ commands += ['mcpp test -p solutions', 'd2x checker']
+ bootstrap += '\nxlings update\nxlings install --use d2x@' + pins['d2x'] + ' -y\n'
+ if os.name == 'nt':
+ bootstrap += "if ($LASTEXITCODE -ne 0) { throw 'd2x bootstrap failed' }\n"
+assert commands[-2:] == ['mcpp test -p solutions', 'd2x checker']
+assert 'xlings install -y' in commands
+assert not any('@latest' in command for command in commands)
+
assert not WORK.exists(), 'Use a fresh disposable checkout'
assert not (ROOT / '.d2x/state.json').exists(), 'Use a checkout without learning progress'
@@ -80,14 +95,14 @@ def readme_commands(name, start, end):
if command == 'd2x install d2mcpp' and os.environ.get('D2X_CANDIDATE'):
command = "& '" + os.environ['D2X_CANDIDATE'].replace("'", "''") + "' install d2mcpp"
body += command + "\nif ($LASTEXITCODE -ne 0) { throw 'README command failed' }\n"
- if command == 'xlings install --use d2x@latest mcpp@latest -y':
- body += f"python '{ROOT / 'scripts/ci/versions.py'}' --public '{LOGS / 'public-tool-versions.json'}'\nif ($LASTEXITCODE -ne 0) {{ throw 'Public version snapshot failed' }}\n"
+ if command == 'xlings install -y':
+ body += f"python '{ROOT / 'scripts/ci/versions.py'}' --public '{LOGS / 'installed-tool-versions.json'}'\nif ($LASTEXITCODE -ne 0) {{ throw 'Installed version snapshot failed' }}\n"
script.write_text(body, encoding='utf-8')
shell = ['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', str(script)]
else:
import shlex
- offset = commands.index('xlings install --use d2x@latest mcpp@latest -y') + 1
- commands.insert(offset, 'python3 ' + shlex.quote(str(ROOT / 'scripts/ci/versions.py')) + ' --public ' + shlex.quote(str(LOGS / 'public-tool-versions.json')))
+ offset = commands.index('xlings install -y') + 1
+ commands.insert(offset, 'python3 ' + shlex.quote(str(ROOT / 'scripts/ci/versions.py')) + ' --public ' + shlex.quote(str(LOGS / 'installed-tool-versions.json')))
if os.environ.get('D2X_CANDIDATE'):
import shlex
commands = [shlex.quote(os.environ['D2X_CANDIDATE']) + ' install d2mcpp' if c == 'd2x install d2mcpp' else c for c in commands]
@@ -110,11 +125,12 @@ def readme_commands(name, start, end):
assert expected > 0 and re.findall(r'(\d+) passed; (\d+) failed', output)[-1] == (str(expected), '0')
assert f'FIRST-RUN PASS: {expected} exercises' in output
actual = subprocess.check_output(['git', '-C', str(course), 'rev-parse', 'HEAD'], text=True).strip()
+ subprocess.run(['git', '-C', str(course), 'diff', '--exit-code', '--', '.xlings.json'], check=True)
if args.published:
assert json.loads((course / '.xlings.json').read_text()) == json.loads((ROOT / '.xlings.json').read_text()), 'Published course pins are stale'
else:
assert actual == head, f'Course download used {actual}, expected candidate {head}'
- (LOGS / 'readme-result.json').write_text(json.dumps({'candidate': head, 'downloaded': actual, 'published': args.published, 'status': 'passed'}, indent=2), encoding='utf-8')
+ (LOGS / 'readme-result.json').write_text(json.dumps({'candidate': head, 'downloaded': actual, 'published': args.published, 'route': 'd2x' if args.d2x_download else 'git', 'status': 'passed'}, indent=2), encoding='utf-8')
finally:
if (course / '.d2x/ci-logs').exists():
shutil.copytree(course / '.d2x/ci-logs', LOGS / 'readme-events', dirs_exist_ok=True)
diff --git a/scripts/ci/versions.py b/scripts/ci/versions.py
index 7dad16b..530a5f2 100644
--- a/scripts/ci/versions.py
+++ b/scripts/ci/versions.py
@@ -17,11 +17,11 @@ def checker_binary():
def verify(root=ROOT, label='versions'):
pins = json.loads((root / '.xlings.json').read_text())['workspace']
result = {'pins': pins, 'cwd': str(root), 'tools': {}}
- for name in ['d2x', 'mcpp']:
+ for name in pins:
actual = subprocess.check_output([name, '--version'], cwd=root, text=True, encoding='utf-8').strip()
path = shutil.which(name)
result['tools'][name] = {'version': actual, 'path': path}
- assert actual.split()[-1] == pins[name], f'{name}: actual {actual}; expected {pins[name]}; path {path}'
+ assert actual.split()[-1].removeprefix('v') == pins[name], f'{name}: actual {actual}; expected {pins[name]}; path {path}'
if os.environ.get('D2X_CANDIDATE'):
binary = Path(os.environ['D2X_CANDIDATE'])
provenance = json.loads(binary.with_name('provenance.json').read_text())