1. SWAT 源码编译为什么总卡在 gfortran 和 Makefile 上如果你正在 Windows 上折腾 SWAT 水文模型源码大概率会遇到这样的场景下载完源码打开 MSYS2 终端敲下make然后屏幕上滚出一堆Error: Unexpected end of file或者Duplicate definition of variable编译进程直接中断。这不是你操作有问题而是 SWAT 这套 Fortran 代码本身带着历史包袱——它最初是为 Intel Fortran 和 Unix 环境写的换到 gfortran Windows 组合后换行符、行长度限制、模块重复包含这些细节全都会跳出来捣乱。SWATSoil and Water Assessment Tool是一套流域尺度水文水质模拟工具源码以 Fortran 为主包含大量.f和.f90文件。它的编译流程本质上就是用 gfortran 把每个源文件编译成.o对象文件再把模块文件mod*.f单独编译成.mod最后链接成swat.exe。听起来简单但 Makefile 里如果没处理好模块依赖顺序或者源码里有跨行注释、重复 include编译就会在某个文件上卡死。这篇文章面向的是已经拿到 SWAT 源码、装了 MSYS2、但被 Makefile 报错拦住的人。我会给出可复制的 MSYS2 环境变量配置、完整的 Makefile 片段、以及编译日志归因时怎么用 TaoToken 统一 Key 调 API 做辅助排查。目标很明确——让你一次跑通swat.exe。适合谁看水文/环境专业的研究生、做流域模拟的工程师、以及任何需要在 Windows 上编译 Fortran 科学计算源码的人。不需要你精通 Fortran但需要你能看懂终端报错、会复制粘贴命令。先说结论SWAT 编译失败的高频原因就三个——Makefile 里模块编译顺序不对、源码跨行注释被 gfortran 拒绝、main.f里重复 include 了modparm.f。下面逐个拆。2. TaoToken 前置准备统一 Key 打通构建日志归因链路在正式编译之前我想先解决一个容易被忽略的问题编译报错日志的归因效率。SWAT 源码有几百个文件make一跑报错信息可能几十行有些是 warning 可以忽略有些是 error 必须改。如果你只靠肉眼一行行看很容易漏掉关键信息。我的做法是把编译日志丢给大模型做归因分析让它帮我区分「哪些 error 是致命的」「哪些 warning 可以跳过」「某个 error 对应源码哪一行」。这样比自己在几百行日志里翻要快得多。这里用到 TaoToken 的统一 Key。TaoToken 是一个 API 聚合平台你可以用同一个 Key 调用多个模型不需要为每个模型单独申请账号。对于编译日志归因这种场景我会用它的模型对话接口。前置准备分三步第一步获取 API Key。访问 TaoToken 控制台的 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite创建一个 Key。这个 Key 后面会用在构建脚本里用来调用模型做日志分析。第二步确认 Base URL。TaoToken 的 API 端点是https://taotoken.net/api注意这个地址不带 UTM 参数是纯 API 入口。你在任何 HTTP 客户端里配置时Base URL 填这个。第三步选模型。对于编译日志归因我建议用推理能力强的模型。你可以在模型对话页面https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite看当前可用的模型列表。选一个上下文窗口够大的因为编译日志可能很长。为什么要用统一 Key因为编译排查过程中你可能需要切换不同模型来对比归因结果。如果每个模型都要单独申请 Key、单独配环境变量切换成本太高。统一 Key 的好处是一个 Key、一个 Base URL换模型只改一个 model 参数。这里给一个最小可用的调用示例用 curl 测试 Key 是否生效curl https://taotoken.net/api/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -d { model: claude-sonnet-4-20250514, messages: [ {role: user, content: SWAT编译报错Error: Unexpected end of file in grow.f line 335怎么定位} ] }如果返回正常 JSON说明 Key 和 Base URL 都配对了。注意$TAOTOKEN_API_KEY是你环境变量里存的 Key不要直接硬编码在脚本里。对于长期做 SWAT 编译和调试的人可以考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite它适合需要反复调用 API 做代码分析的场景。如果你只是偶尔排查一次编译问题用按量计费的 API Key 就够了。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有各语言的 SDK 示例。我下面会用 Python 写一个日志归因脚本你可以直接复制。3. 可复制配置MSYS2 环境变量与 SWAT Makefile 完整片段这一节是核心操作部分。我会给出三样东西MSYS2 环境变量配置、SWAT 的 Makefile 完整内容、以及一个把编译日志发给 TaoToken 做归因的 Python 脚本。3.1 MSYS2 环境变量配置假设你把 MSYS2 装在默认路径C:\msys64gfortran 装在C:\msys64\mingw64\bin。你需要把这个路径加到 Windows 的 PATH 里这样在任意终端都能调用gfortran。在 MSYS2 的 MINGW64 终端里先确认 gfortran 可用which gfortran # 应该输出 /mingw64/bin/gfortran gfortran --version # 应该输出 GNU Fortran (GCC) 13.x.x 或类似版本如果which gfortran没输出说明没装。用 pacman 装pacman -S mingw-w64-x86_64-gcc-fortran pacman -S make装完后把C:\msys64\mingw64\bin加到 Windows 系统环境变量 PATH 里。这样你在 CMD 或 PowerShell 里也能用 gfortran。在 MSYS2 终端里可以设置一个临时环境变量指向 TaoToken 的 APIexport TAOTOKEN_API_KEY你的Key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你想让这个环境变量永久生效把它加到~/.bashrc里echo export TAOTOKEN_API_KEY你的Key ~/.bashrc echo export TAOTOKEN_BASE_URLhttps://taotoken.net/api ~/.bashrc source ~/.bashrc3.2 SWAT Makefile 完整片段在 SWAT 源码根目录下新建一个文件命名为Makefile没有扩展名。内容如下你可以直接复制# SWAT makefile for gfortran on MSYS2 # Usage: # make all - build debug and release # make release - build release only # make debug - build debug only # make clean - remove build artifacts # make prep - create debug/release directories MODS : $(wildcard mod*.f) MOD_OBJS : $(patsubst %.f,%.o,$(MODS)) SRCS : $(wildcard *.f *.f90) OBJS : $(patsubst %.f,%.o,$(patsubst %.f90,%.o,$(SRCS))) FC gfortran PROGRAM swat DBGDIR debug DBGEXE $(DBGDIR)/$(PROGRAM) DBGOBJS $(addprefix $(DBGDIR)/, $(OBJS)) DBGMODS $(addprefix $(DBGDIR)/, $(MOD_OBJS)) DBGCFLAGS -c -O0 -static -ffixed-line-length-none -ffree-line-length-none -stdgnu -fno-automatic -fcheckall -fbacktrace DBGLFLAGS -g RELDIR release RELEXE $(RELDIR)/$(PROGRAM) RELOBJS $(addprefix $(RELDIR)/, $(OBJS)) RELMODS $(addprefix $(RELDIR)/, $(MOD_OBJS)) RELCFLAGS -c -O3 -static -ffixed-line-length-none -ffree-line-length-none -stdgnu -fno-automatic -ffpe-summarynone RELLFLAGS -g .PHONY: all clean debug prep release remake all: prep debug release release: $(RELEXE) $(RELEXE): $(RELMODS) $(RELOBJS) $(FC) $(RELLFLAGS) -o $(RELEXE) $^ $(RELDIR)/%.o: %.f $(FC) $(RELCFLAGS) -o $ $ $(RELDIR)/%.o: %.f90 $(FC) $(RELCFLAGS) -o $ $ debug: $(DBGEXE) $(DBGEXE): $(DBGMODS) $(DBGOBJS) $(FC) $(DBGLFLAGS) -o $(DBGEXE) $^ $(DBGDIR)/%.o: %.f $(FC) $(DBGCFLAGS) -o $ $ $(DBGDIR)/%.o: %.f90 $(FC) $(DBGCFLAGS) -o $ $ clean: rm -f $(RELEXE) $(RELOBJS) $(DBGEXE) $(DBGOBJS) prep: mkdir -p $(DBGDIR) $(RELDIR) remake: clean all这个 Makefile 的关键点MODS用wildcard mod*.f抓取所有模块文件MOD_OBJS把它们转成.o。SRCS抓取所有.f和.f90。FC gfortran指定编译器。DBGCFLAGS里的-ffixed-line-length-none和-ffree-line-length-none是必须的因为 SWAT 有些行超过 72 字符gfortran 默认会截断。-fno-automatic让局部变量不自动变成 static避免一些隐式保存问题。注意DBGCFLAGS里我去掉了-ffpe-trapinvalid,zero,overflow因为这个标志会让浮点异常直接崩溃调试阶段反而不好定位。如果你需要严格检查浮点错误可以加回去。3.3 编译日志归因脚本下面这个 Python 脚本会把make的输出日志发给 TaoToken让模型帮你分析哪些 error 必须修、哪些 warning 可以忽略。import os import subprocess import requests TAOTOKEN_API_KEY os.environ.get(TAOTOKEN_API_KEY) TAOTOKEN_BASE_URL os.environ.get(TAOTOKEN_BASE_URL, https://taotoken.net/api) def run_make(): result subprocess.run( [make, release], capture_outputTrue, textTrue, cwd. ) return result.stdout \n result.stderr def analyze_log(log_text): url f{TAOTOKEN_BASE_URL}/chat/completions headers { Content-Type: application/json, Authorization: fBearer {TAOTOKEN_API_KEY} } payload { model: claude-sonnet-4-20250514, messages: [ { role: system, content: 你是Fortran编译专家。用户会给你SWAT源码的make编译日志。请分类列出1) 必须修复的error及其文件和行号2) 可以忽略的warning3) 修复建议。用中文回答。 }, { role: user, content: f以下是编译日志\n\n{log_text[:8000]} } ] } resp requests.post(url, headersheaders, jsonpayload, timeout60) resp.raise_for_status() return resp.json()[choices][0][message][content] if __name__ __main__: log run_make() print( 编译日志 ) print(log[:2000]) print(\n 归因分析 ) print(analyze_log(log))这个脚本做了两件事跑make release抓日志然后把日志发给 TaoToken 做归因。注意log_text[:8000]是截断防止日志太长超出模型上下文。如果你的日志超过 8000 字符可以先 grep 出 error 行再发。运行方式export TAOTOKEN_API_KEY你的Key python analyze_build.py如果你用的是 Claude Code 做开发可以把 TaoToken 的 Base URL 配到 Claude Code 的 settings 里。具体配置参考接入文档核心就是三件套Base URL 填https://taotoken.net/apiKey 填你的 TaoToken KeyModel ID 填你选的模型。4. 验证请求与成功结果从 make 报错到 swat.exe 生成配置好 Makefile 和环境变量后进入 SWAT 源码目录执行编译。这一节我会演示完整的编译过程包括遇到的报错和修复动作。4.1 第一次 make预期会报错cd /path/to/swat_source make release第一次跑大概率会在某个文件上报错。我实测下来最常见的两个 error 是报错一跨行注释导致Unexpected end of filegrow.f:335:72: 335 | ! 这是注释 | 1 Error: Unexpected end of file原因SWAT 有些.f文件里注释写在了续行符后面gfortran 解析时认为文件没结束。修复方法把注释和代码合并到一行或者把注释移到单独一行。比如grow.f第 335 行附近如果原来是call some_subroutine(arg1, arg2) ! 注释改成call some_subroutine(arg1, arg2) ! 注释报错二变量重复定义Duplicate definitionmain.f:12:72: 12 | include modparm.f | 1 Error: Duplicate definition of parm at (1)原因Makefile 已经把modparm.f编译成了parm.mod但main.f里又include modparm.f导致重复。修复方法把main.f里那行include modparm.f注释掉。! include modparm.f4.2 用归因脚本辅助定位每次改完源码重新跑make release如果还有报错把日志丢给归因脚本python analyze_build.py脚本会输出类似这样的归因结果 归因分析 必须修复的 error 1. grow.f:335 - 跨行注释导致文件提前结束建议合并注释行 2. main.f:12 - modparm.f 重复 include建议注释掉该行 可以忽略的 warning - 多个文件出现 Warning: Line truncated at (1)因为已加 -ffree-line-length-none不影响 修复建议 先修 grow.f再修 main.f然后 make clean make release这样你就不用自己一行行翻日志了。4.3 成功编译修完所有 error 后执行make clean make release如果一切正常终端最后会输出gfortran -g -o release/swat debug/... release/...然后在release/目录下会生成swat.exe。验证ls -lh release/swat.exe # 应该看到类似 -rwxr-xr-x 1 user 197121 2.3M ... release/swat.exe4.4 测试 swat.exe把swat.exe复制到 SWAT 项目的TxtInOut目录下这个目录包含输入文件然后在命令行运行cd /path/to/TxtInOut ./swat.exe如果模型开始读取输入文件并输出SWAT的日志信息说明编译成功。你会看到类似SWAT 2012 Rev. 664 Reading input files... ... Simulation complete.到这一步SWAT 可执行文件就跑通了。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth编译过程中除了 Fortran 源码本身的报错还可能遇到 API 调用相关的错误。这一节列出我踩过的坑和排查方法。5.1 401 Unauthorized如果你在跑归因脚本时看到requests.exceptions.HTTPError: 401 Client Error: Unauthorized原因API Key 没配、配错、或者环境变量没生效。排查步骤echo $TAOTOKEN_API_KEY # 应该输出你的Key如果为空说明没配如果为空重新 exportexport TAOTOKEN_API_KEY你的Key如果 Key 有值但还是 401检查 Key 是否过期或被删除。去 TaoToken 控制台的 API Keys 页面确认。5.2 local proxy failed如果你看到local proxy failed: connection refused或者类似的连接错误原因通常是 Base URL 配错了。检查echo $TAOTOKEN_BASE_URL # 应该是 https://taotoken.net/api注意不要写成https://taotoken.net/api/末尾多斜杠也不要在 Base URL 里带 UTM 参数。API 端点就是https://taotoken.net/api。5.3 reading choices 报错如果你在解析 API 返回时看到KeyError: choices或者reading choices failed: list index out of range原因API 返回的 JSON 结构和你预期的不一样。可能是模型名写错了或者请求格式不对。排查先打印原始返回resp requests.post(url, headersheaders, jsonpayload) print(resp.status_code) print(resp.text)如果返回里有error字段看错误信息。常见的是model not found说明模型 ID 写错了。去模型对话页面确认可用的模型 ID。5.4 OAuth 相关报错如果你用的是 Claude Code 或其他需要 OAuth 的工具可能会看到OAuth token expired或者OAuth authentication failed原因OAuth token 过期了。对于 TaoToken 的 API Key 方式不需要 OAuth直接用 Bearer Token 就行。如果你在 Claude Code 里配置确保用的是 API Key 而不是 OAuth。Claude Code 的配置三件套Base URL:https://taotoken.net/apiAPI Key: 你的 TaoToken KeyModel ID: 你选的模型比如claude-sonnet-4-20250514配置位置在 Claude Code 的 settings 文件里具体路径参考接入文档。5.5 编译报错速查表报错关键词原因修复Unexpected end of file跨行注释合并注释行Duplicate definition重复 include注释掉 main.f 里的 includeLine truncated行超长加 -ffree-line-length-noneUndefined reference链接顺序错确保 mod 文件先编译Permission denied文件被占用关闭编辑器再 make401 UnauthorizedKey 没配export TAOTOKEN_API_KEYlocal proxy failedBase URL 错检查 https://taotoken.net/apireading choices模型名错确认 model ID6. 语义一致 CTA把编译排查链路固化下来SWAT 编译这件事第一次跑通之后后面就是重复劳动。我的建议是把整个链路固化成一个脚本make clean make release抓日志自动发给 TaoToken 做归因如果有 error 就输出修复建议没有就提示编译成功。这样你每次改完源码只需要跑一个命令不用手动看日志。如果你需要频繁做这类编译日志分析可以考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite它适合长期、高频的 API 调用场景。如果只是偶尔排查用 API Keys 按量计费就够了。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite里面有 Python、Node.js、curl 的完整示例。模型列表在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite你可以根据日志长度选上下文窗口合适的模型。最后说一个实用技巧SWAT 源码里有些.f文件用的是固定格式fixed-form有些是自由格式free-form。gfortran 默认按扩展名判断.f是固定格式.f90是自由格式。如果你遇到Non-numeric character in statement label这类报错可能是格式判断错了。可以在 Makefile 里对特定文件加-ffree-form强制指定。编译成功后把swat.exe和TxtInOut目录一起打包下次换机器直接复制不用重新编译。这是我试过最省事的做法。