安装 WSL
什么是 WSL
WSL 是 Windows Subsystem for Linux 的缩写,即“适用于 Linux 的 Windows 子系统”。它让你能在 Windows 上直接运行 Linux 命令行、工具和程序,无需安装虚拟机或双系统。WSL2 使用真实 Linux 内核,性能和兼容性更好。
为什么要安装 WSL
主要还是因为 sagemath 撤销了对 Windows 的支持。另外以后如果跑 agent 的话,在 Windows 环境下 powershell 命令容易被写坏,agent 对 Linux 命令的支持度更高。再者,想学 Linux 命令、测试部署环境,或使用 Docker、Git 等工具时,WSL 是很方便的选择。
使用命令行安装
在安装之前,先查看可用的发行版:
版本有很多。这里为了适配主流 Linux 操作命令,我们安装最新版的 Ubuntu-26.04:
1 wsl --install -d Ubuntu-26.04
安装成功后,会提示让我们创建默认账户。这个账户是 普通的非管理员用户账户 ,但具备执行 sudo 的权限。
1 2 3 4 5 6 7 正在下载: Ubuntu 26.04 LTS 正在安装: Ubuntu 26.04 LTS 已成功安装分发。可以通过 “wsl.exe -d Ubuntu-26.04” 启动它 正在启动 Ubuntu-26.04... Provisioning the new WSL instance Ubuntu-26.04 This might take a while... Create a default Unix user account:
用户名按自己喜欢的来就好,但首字母不能大写。之后如果改名字可能会导致一堆莫名其妙的报错,所以尽量现在想好用户名,以后不要改动。
之后会提示输入密码。因为个人电脑没什么安全风险,所以可以先设一个弱密码,等会再配置使用 sudo 命令时无需输入密码,达到类似无密码的效果。
后面的匿名系统数据收集计划输入 y 同意就好,至此我们就成功进入 Ubuntu 系统,还是非常简单的。
Linux 基础配置
之后我们就可以通过 powershell 的扩展框进入 Linux 终端:
初次进入,可能会有点不习惯终端的配色风格和光标。这些可以在上图扩展框的设置中调整,进入后如下图所示。可以更换图标,也能把光标从方块更改为细线式,更改终端背景颜色(或把背景换为图片),字体大小及样式等。我个人喜欢 Campbell 的配色方案,Cascadia Code 字体,竖线光标。当然,因人而异嘛。
配置好后,前缀大概类似 cheyanne@LAPTOP-GRNTPI58:~$
如果觉得这个主机名看着很别扭,可以自己更改。先编辑第一份配置文件:
在 nano 中写入以下内容(wslname 换成自己喜欢的名字)
1 2 3 [network] hostname = wslname generateHosts = false
写入后按顺序按 ctrl+O,enter,ctrl+X 保存并退出。接着编辑第二份配置文件:
把其中带有你的主机名的部分全部替换为你自己起的主机名,同样按 ctrl+O,enter,ctrl+X 保存并退出。
改完后在 Windows powershell 中 执行 wsl --shutdown,再次进入终端就修改成功了。
类似 cheyanne@ubuntu:~$
需要知道的是,每个 Linux 用户都被分配了一个家目录(用 ~ 表示,根目录用 / 表示)。平常写脚本改代码时尽量在家目录里活动,不要到根目录里随便改文件,容易改崩。等到安装的程序多了,家目录容易变得很乱。我个人推荐在家目录下新建一个文件夹,平常在那里敲代码。文件夹名字假设为 main,新建文件夹:
设置启动终端时默认目录为 ~/main(而非原本的直接进入 ~):
编辑 ~/.bashrc:
末尾新增一行:
保存并退出后使更改生效:
这样以后进入终端的默认目录就是 ~/main 了。
需要的话,配置 sudo 命令无需密码:
在末尾加上这段(username 换成自己的):
1 username ALL=(ALL:ALL) NOPASSWD: ALL
对于 Ubuntu-26.06 ,使用以下命令更换下载源(其它版本不要用这个):
1 sudo sed -i 's|http://archive.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g; s|http://security.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources
再更新软件源(换源后下载速度应该快很多):
1 2 3 sudo apt update sudo apt upgrade -y sudo apt install -y build-essential git curl wget unzip zip ca-certificates gdb file patchelf
还有一些基础的 Linux 命令(比如 cd,ls)和快捷键(比如 ctrl+L 清空终端,ctrl+A 将光标移动到输入框开头等)需要大家自己学习,这里就不再赘述了。
安装 conda 及 sagemath
什么是 conda
Conda 是一个开源的包管理和环境管理工具,主要用于安装和管理软件包及其依赖。它能自动处理依赖关系,让安装、更新和卸载更加方便,同时还可以为不同项目创建独立的运行环境,有效避免库版本冲突。
安装 conda
安装过程中一路 yes 或者回车就好,这里安装的是 Miniforge3:
1 2 wget https://mirrors.ustc.edu.cn/github-release/conda-forge/miniforge/LatestRelease/Miniforge3-Linux-x86_64.sh bash Miniforge3-Linux-x86_64.sh
完成之后执行:
1 2 rm Miniforge3-Linux-x86_64.sh source ~/.bashrc
安装成功之后前缀会有一个 (base) 标记。配置:
1 2 3 conda config --set channel_priority strict conda config --set solver libmamba conda config --show channels
安装 sagemath
用 mamba 创建 sage 环境:
1 mamba create -y -n sage sage
验证能否进入 sage 环境:
如果能看到 (sage) 前缀,那么恭喜你,最难的任务已经完成了 🥰
真实使用时,我们不需要激活环境,直接指名道姓使用就可以。先退出 sage 环境:
如果退到了 base 环境,再执行一次上面的命令就行。
关闭自动启动 base:
1 conda config --set auto_activate_base false
测试能否通过绝对路径使用 sagemath:
1 ~/miniforge3/envs/sage/bin/sage
界面是这样的:
可以测试一些简单的命令,验证能不能正常使用:
1 2 3 4 print("Hello, Hnusec!") 1 + 1 factor(91) # 91的质因数分解 quit() # 退出交互
每次都得输绝对路径太麻烦了,我们在配置中设置 sage 命令对路径 ~/miniforge3/envs/sage/bin/sage 的映射(虽然一般不直接在这个交互界面使用 sage):
1 2 3 4 mkdir -p ~/.local/bin ln -s ~/miniforge3/envs/sage/bin/sage ~/.local/bin/sage echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc source ~/.bashrc
这样以后直接输入 sage 就可以进交互终端,不用输绝对路径了。
安装第三方模块
如果一切正常,现在 Ubuntu 里应该有三个 python 环境:系统 python、conda-base 的 python,以及 sagemath 内置的 python。
切记不要在终端里直接 pip install ...,此时操作的是系统 python,会报错如下。不要动系统 python。
conda-base 的 python 虽然可以用,但是没什么必要(毕竟写一般的 python 脚本都在 Win 里,且 sage 的 python 完全可以承担它的功能),所以我们安装第三方模块都是在 sage 的 python 里。
使用以下命令在 sage 的 python 安装第三方模块:
1 ~/miniforge3/bin/mamba run -n sage pip install 包名
下面的命令一般不使用。优先用上面的 pip 安装:
1 ~/miniforge3/bin/mamba install -n sage 包名
举个例子。在没安装 pycryptodome 库之前,直接导入会报错:
使用命令安装:
1 ~/miniforge3/bin/mamba run -n sage pip install pycryptodome
之后就可以正常使用库函数:
配置 VSCode 及其扩展
首先确保你的 Win 系统中已经有 VSCode。在 Win 的 VSCode 中下载 WSL 扩展:
安装好后在 Ubuntu 终端中输入:
后面的 . 代表把当前目录设为工作区,当然也可以打开别的目录。
除了在 Ubuntu 终端中打开 VSCode,也可以在 Win 的 VSCode 中打开 Ubuntu 的 VSCode,如图:
如果你的 VSCode 设置了窗口缩放,启动 WSL 后缩放就没有了,可以在 Win 中 VSCode 的 settings.json 末尾新增 "window.zoomLevel": 1, 以锁定窗口缩放。
对于密码方向来说,WSL 的 VSCode 主要是用来写 python 和 sage 的,扩展不用装太多。推荐这些:
Python 全家桶(Python、Pylance、Black Formatter 等等)
SageMath Enhanced(对于 sage 脚本生效,简单的语法高亮补全和一键运行)
Jupyter(虽然我没怎么用过)
安装好扩展后可以按 ctrl+shift+P 重加载窗口。关于 VSCode 的快捷键也有非常多,用好了会很方便。
再次按 ctrl+shift+P 选择 Python 解释器为 sage,如图:
语法差异
如果题目刚需 sage 环境(比如涉及到 coppersmith、ECC 以及格密码时),只依靠交互环境是很费劲的,这个时候就需要编写 sage 脚本解题。但是目前 .sage 没有成熟的代码补全和高亮。虽然 python 能导入 sage 库(from sage.all import *),能够享受 lsp,但是语法却有许多限制。对比一下 sage 写法和 python 写法的差异:
对于精确有理数(如
1 3
):sage 写 a = 1/3 能精确保留,python 需要额外写成 a = QQ(1)/3
幂运算:sage 用 ^(异或是 ^^),python 用 **(异或是 ^)
单变量多项式环:R.<x> = QQ[],python中取生成元是x = QQ["x"].gen(),取环是R = QQ["x"]。二者都要的话要写成两句:R = QQ["x"]; x = R.gen()
类比所有带 .<x> 形式的语法,python 都不能直接用(如多变量多项式环,商环,数域等等)
连续步长的列表:[1..5],这个语法糖的意味太重了,python 是 list(range(1, 5+1))
大概就只有这么多,其它的 sage 语法在 python 编辑器里不会报错,可以照常写。
直接写 .sage 脚本可以享受更便捷的语法,但是失去了高亮和补全。写 .py (这里指调用 sage 库函数的脚本)正好相反,语法复杂一些,但是体验更好。
修改扩展(可选)
SageMath Enhanced 扩展有些地方用着不是很顺手:
当你试图按住 ctrl 以查看类 / 方法的定义时(其实更可能是按快捷键时鼠标不小心移到了上面),扩展会报错 Request textDocument/definition failed.,且这个报错不能静默。
写矩阵时如果分行写会报警告(原因是原扩展只维护了同一行的括号栈),Unmatched parentheses on line x
点击右上角的 Run SageMath File 小箭头后文件不会自动保存,只按照旧脚本运行。
(可选的优化)保存时若代码末尾无空行,则新增一个空行并将光标移动到该空行上。
接下来的扩展修复均基于目录 ~/.vscode-server/extensions/lov3.sagemath-enhanced-2.0.8,一定在修复之前备份原扩展。
每次修改后记得使用 ctrl+shift+P 重载窗口以使修改生效。
语言服务器请求错误
文件:
1 out/server/src/server.js
在 out/server/src/server.js:113,服务端声明支持 textDocument/definition,但文件中没有注册 connection.onDefinition(...)。Ctrl+悬停触发定义请求时,服务端返回“方法未实现”,于是 VSCode 显示 Request textDocument/definition failed.
直接删除该能力声明。这样 VSCode 不再向此语言服务器发送定义请求,错误消失。
多行矩阵误报 Unmatched parentheses on line x
文件:
1 out/server/src/server.js
修改前
在 documents.onDidChangeContent(...) 后面直接进入 validateTextDocument,并在 validateTextDocument 的循环里逐行数括号。原逻辑大致在 validateTextDocument 内:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const diagnostics = [];const lines = text.split ('\n' );for (let i = 0 ; i < lines.length ; i++) { const line = lines[i]; const openParens = (line.match (/\(/g ) || []).length ; const closeParens = (line.match (/\)/g ) || []).length ; if (openParens !== closeParens) { const diagnostic = { severity : node_1.DiagnosticSeverity .Warning , range : { start : { line : i, character : 0 }, end : { line : i, character : line.length } }, message : `Unmatched parentheses on line ${i + 1 } ` , source : 'sagemath-enhanced' }; diagnostics.push (diagnostic); }
修改后
在 documents.onDidChangeContent(...) 和 async function validateTextDocument(...) 之间新增以下函数和常量。位置约为 out/server/src/server.js:187:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 const OPENING_DELIMITERS = { '(' : ')' , '[' : ']' , '{' : '}' };const CLOSING_DELIMITERS = { ')' : '(' , ']' : '[' , '}' : '{' };function createDelimiterDiagnostic (line, character, lineLength, message ) { return { severity : node_1.DiagnosticSeverity .Warning , range : { start : { line, character }, end : { line, character : Math .min (character + 1 , Math .max (lineLength, character + 1 )) } }, message, source : 'sagemath-enhanced' }; }function collectDelimiterDiagnostics (lines ) { const diagnostics = []; const stack = []; let stringState = undefined ; for (let lineNumber = 0 ; lineNumber < lines.length ; lineNumber++) { const line = lines[lineNumber]; for (let character = 0 ; character < line.length ; character++) { const char = line[character]; if (stringState) { if (stringState.escaped ) { stringState.escaped = false ; continue ; } if (char === '\\' ) { stringState.escaped = true ; continue ; } if (stringState.triple ) { if (line.startsWith (stringState.quote .repeat (3 ), character)) { character += 2 ; stringState = undefined ; } } else if (char === stringState.quote ) { stringState = undefined ; } continue ; } if (char === '#' ) { break ; } if (char === '"' || char === "'" ) { const triple = line.startsWith (char.repeat (3 ), character); stringState = { quote : char, triple, escaped : false }; if (triple) { character += 2 ; } continue ; } if (OPENING_DELIMITERS [char]) { stack.push ({ char, line : lineNumber, character }); continue ; } if (CLOSING_DELIMITERS [char]) { const opener = stack[stack.length - 1 ]; if (opener && opener.char === CLOSING_DELIMITERS [char]) { stack.pop (); continue ; } diagnostics.push (createDelimiterDiagnostic (lineNumber, character, line.length , `Unmatched closing delimiter '${char} ' on line ${lineNumber + 1 } ` )); } } if (stringState && !stringState.triple ) { stringState = undefined ; } } for (const opener of stack) { diagnostics.push (createDelimiterDiagnostic (opener.line , opener.character , lines[opener.line ].length , `Unmatched opening delimiter '${opener.char} ' on line ${opener.line + 1 } ; expected '${OPENING_DELIMITERS[opener.char]} '` )); } return diagnostics; }
然后把 validateTextDocument 开头的括号诊断逻辑改成调用新函数。位置约为 out/server/src/server.js:274:
1 2 3 4 5 6 7 const diagnostics = [];const lines = text.split ('\n' ); diagnostics.push (...collectDelimiterDiagnostics (lines));for (let i = 0 ; i < lines.length ; i++) { const line = lines[i];
也就是删除原来的逐行 openParens/closeParens 统计块。
点击 Run SageMath File 前自动保存
文件:
修改前
当前命令回调原来是同步函数,且校验 Sage 文件后直接读取磁盘路径运行。原逻辑大致在 out/src/extension.js:34:
1 2 3 4 5 6 7 8 9 10 11 12 const runDisposable = vscode.commands .registerCommand ('runsagemathfile.run' , () => { const editor = vscode.window .activeTextEditor ; if (!editor) { console .log ('No editor is active' ); return ; } const document = editor.document ; if (document .languageId !== 'sage' ) { vscode.window .showInformationMessage ('The active file is not a SageMath file' ); return ; } const sagePath = vscode.workspace .getConfiguration ().get ('sagemathEnhanced.interpreterPath' );
修改后
把回调改成 async,并在读取 sagePath 前保存脏文档。修改后位置约为 out/src/extension.js:63:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const runDisposable = vscode.commands .registerCommand ('runsagemathfile.run' , async () => { const editor = vscode.window .activeTextEditor ; if (!editor) { console .log ('No editor is active' ); return ; } const document = editor.document ; if (document .languageId !== 'sage' ) { vscode.window .showInformationMessage ('The active file is not a SageMath file' ); return ; } if (document .isDirty ) { const saved = await document .save (); if (!saved) { vscode.window .showErrorMessage ('SageMath file was not saved. Run cancelled.' ); return ; } } const sagePath = vscode.workspace .getConfiguration ().get ('sagemathEnhanced.interpreterPath' );
后续构造终端命令的代码保持不变。
保存时补末尾空行并移动光标
文件:
修改前
function activate(context) { 后直接注册运行命令:
1 2 3 function activate (context ) { const runDisposable = vscode.commands .registerCommand ('runsagemathfile.run' , async () => {
并且订阅列表原来只有运行命令和重启命令:
1 context.subscriptions .push (runDisposable, restartDisposable);
修改后
在 function activate(context) { 后新增保存监听。修改后位置约为 out/src/extension.js:33:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 function activate (context ) { const moveCursorAfterSave = new Set (); const ensureTrailingBlankLineDisposable = vscode.workspace .onWillSaveTextDocument (event => { const document = event.document ; if (document .languageId !== 'sage' ) { return ; } const text = document .getText (); if (!text || text.endsWith ('\n' ) || text.endsWith ('\r' )) { return ; } const lastLine = document .lineAt (document .lineCount - 1 ); const endPosition = new vscode.Position (document .lineCount - 1 , lastLine.text .length ); const eol = document .eol === vscode.EndOfLine .CRLF ? '\r\n' : '\n' ; moveCursorAfterSave.add (document .uri .toString ()); event.waitUntil (Promise .resolve ([vscode.TextEdit .insert (endPosition, eol)])); }); const moveCursorToTrailingBlankLineDisposable = vscode.workspace .onDidSaveTextDocument (document => { const uri = document .uri .toString (); if (!moveCursorAfterSave.delete (uri)) { return ; } const editor = vscode.window .activeTextEditor ; if (!editor || editor.document .uri .toString () !== uri) { return ; } const position = new vscode.Position (document .lineCount - 1 , 0 ); editor.selection = new vscode.Selection (position, position); editor.revealRange (new vscode.Range (position, position)); });
并把订阅列表改为 out/src/extension.js:130 的形式:
1 context.subscriptions .push (ensureTrailingBlankLineDisposable, moveCursorToTrailingBlankLineDisposable, runDisposable, restartDisposable);