☰
Cadence SKILL环境搭建、版本对齐与load避坑指南
2026/10/28 19:12:37 网站建设 项目流程

简介:这份《Cadence SKILL Language User Guide》中文翻译版由机器翻译整理,面向英语阅读有困难、但具备一定C语言基础的技术人员,提供产品版本6.1.6(2014年11月)SKILL语言使用指南的对照参考。内容覆盖SKILL语言概述、与Lisp的关系、语法示例与数据类型标识符、安装与环境配置、编程约定及学习资源指引,并保留前言、许可条款、安装指南、编程指南等章节结构,便于按主题检索,对电路验证、布局编辑等自动化任务也有参考意义。资源包仅含1个PDF文件,大小约68.23MB,适合作为查阅型文档在离线环境中使用。目前已有527人学习。读者可借助它理解SKILL在集成电路设计自动化中的脚本定位,对照示例熟悉基本语法和核心概念,并结合环境变量、编译器安装等说明完成工具链准备;书中对文档结构、官方文档、社区论坛与技术支持的提示,也便于后续查证和深入学习。

1. 装 SKILL 这件事,最容易在版本上翻车

装 SKILL 最反直觉的地方是:翻遍安装介质也找不到一个叫 SKILL 的安装包。它是嵌在设计工具里的解释器,随工具版本一起发布,手上这份《Cadence SKILL 语言使用指南》对应产品版本 6.1.6、2014 年 11 月,属于工具链自带的文档体系,不存在「单独装一个 6.1.6 解释器」这回事。真正会卡住人的是两边对不上:工具是新的,照着旧文档写prog和defmacro,语法能过,行为不对;工具是旧的,文档里的参数修饰符又认不出来。加上这份中文版是机器翻译的产物,术语前后不一致,报错信息和文档对不上号,排查时会多绕好几圈。这篇记录的是从零把这套环境搭起来、把文档当手册用、最后能 load 通一段源码的完整过程,适合刚接手 SKILL 脚本维护、英语读原版吃力的人。

2. 环境准备:工具套件与 SKILL 解释器的版本对齐

SKILL 的运行环境不是独立的,它跟着设计工具套件走。所以「安装 SKILL」这个动作,实际拆成两件事:装好工具套件,再把 SKILL 的加载路径和启动入口配明白。这一步做对了,后面写多少代码都是顺的;这一步糊弄过去,后面每个报错都会指向错误的方向。

2.1 先确认工具链版本,再决定看哪一版文档

6.1.6 这个版本号不是随便标的,它对应的是工具链的主版本号,文档、解释器、内置函数库三者是对齐的。我在机器上做的第一件事永远是确认工具套件装的是哪个版本,而不是先打开文档。

# 确认工具套件安装根目录,版本号一般在路径末段 ls -d /opt/cadence/IC* # 常见输出:/opt/cadence/IC616 /opt/cadence/IC617 # 确认可执行文件是否存在,以及它自报的版本 which virtuoso virtuoso -version

第一条命令看的是安装根目录,IC616这种命名方式里,616就是主版本号,跟文档的 6.1.6 是同一套编号逻辑。第二条命令验证可执行文件确实在 PATH 里,并且自报版本与目录名一致——不一致的情况我见过,多半是 PATH 里混进了另一套旧安装,which出来的路径不是你以为的那个。

注意:版本号只认工具自报的那一个。目录名可以改,环境变量可以指错,-version的输出才是准的。

如果工具是 6.1.7 之后的版本,文档里 6.1.6 的语法基本仍然适用,但新增的参数修饰符和内置函数在原文档里找不到,这时候要以工具自带的在线帮助为准,中文指南只当语法骨架用。反过来,工具比 6.1.6 还旧,就老老实实按文档写,不要用新版本才有的写法,否则解释器直接报未定义函数。

2.2 三个环境变量:路径、可执行文件与加载目录

环境变量是这套环境里最容易埋雷的地方,因为它不会立刻报错,而是让你在半小时后发现「代码明明对,就是跑不起来」。我一般只动三处,其余交给工具自带的启动脚本。

# 1) 安装根目录,供其他脚本引用 export CDS_INST_DIR=/opt/cadence/IC616 # 2) 把工具的可执行目录塞进 PATH,顺序放在系统目录之前 export PATH=$CDS_INST_DIR/tools/bin:$CDS_INST_DIR/tools/dfII/bin:$PATH # 3) 自己写的 .il 源码放这里,供 SKILL 的加载路径引用 export MY_SKILL_LIB=$HOME/skill/lib

第一项CDS_INST_DIR是个约定俗成的变量名,很多启动脚本会读它,值必须是安装根目录而不是tools子目录,这点写错会导致后续脚本找不到库。第二项把两个 bin 目录前置,tools/bin放通用可执行文件,tools/dfII/bin放图形界面入口,顺序很重要——如果系统里另有同名命令,前置才能保证调用的是工具自带的那个。第三项是我自己的习惯,把个人源码目录单独抽出来一个变量,后面在 SKILL 里拼路径时直接读环境变量,不用写死绝对路径,换机器时只改这一行。

# 写进 shell 配置文件,避免每次开终端都要重设 echo 'export CDS_INST_DIR=/opt/cadence/IC616' >> ~/.bashrc echo 'export PATH=$CDS_INST_DIR/tools/bin:$CDS_INST_DIR/tools/dfII/bin:$PATH' >> ~/.bashrc echo 'export MY_SKILL_LIB=$HOME/skill/lib' >> ~/.bashrc source ~/.bashrc

追加到~/.bashrc是为了持久化,注意这里的引号必须是单引号,让变量在 shell 启动时才展开,写双引号会在写入的那一刻就把值固定死,后面改CDS_INST_DIR就不生效了。source之后用echo $CDS_INST_DIR验一下,输出了正确路径再往下走。

除了 shell 环境变量,SKILL 自己还维护一条加载路径,叫skillPath。它的作用类似其他语言的模块搜索路径,load一个相对路径的文件时会在这条路径里找。我的做法是在启动脚本里追加个人目录,而不是直接覆盖:

; 把个人源码目录追加到 SKILL 加载路径的最前面 setSkillPath( append( list( getShellEnvVar("MY_SKILL_LIB") ) getSkillPath() ) ) ; 打印出来确认,返回的是一个路径列表 getSkillPath()

getShellEnvVar读的是 shell 环境变量,所以前面那个MY_SKILL_LIB必须先存在。append把新目录拼在已有路径前面,这样同名文件优先加载我自己的版本,不会误用工具自带的同名函数——SKILL 允许重定义函数,这个特性方便,也危险。getSkillPath()的返回值打印出来看一眼,路径列表里应该有刚加进去的那一项。

2.3 找到能敲 SKILL 的那三个入口

环境变量配好之后,需要确认三个入口都能用,它们的用途完全不同,别指望一个顶三个。

第一个是图形界面的命令解释窗口。启动主程序后,界面下方有一条输入行,可以直接敲 SKILL 表达式,回车立即求值。这是最适合试探语法的地方,比如敲一个1+1看返回2,敲一个strcat("a" "b")看返回"ab"。它的好处是出错了只影响这一行,不会把环境搞崩。

第二个是交互式的命令行解释器,适合在没有图形界面的机器上做批量验证。

# 交互式进入,退出用 (exit) 或 Ctrl-D skill

进去之后提示符会变,可以逐行敲表达式,也能load文件。我在服务器上做源码检查时基本都用它,比开图形界面轻。

第三个是批处理模式,把一段 SKILL 脚本直接喂给工具执行,跑完就退。

# 无图形界面执行一段脚本,输出直接打到终端 virtuoso -nograph -replay $MY_SKILL_LIB/check.il

-nograph表示不启动图形界面,-replay表示按行重放指定文件。这个组合非常适合放到自动化流程里做回归检查:改完源码,跑一遍,看输出有没有异常。缺点是出错时的错误信息不如交互式清楚,定位要靠脚本里自己加的打印语句。

提示:三个入口共用同一套环境变量和同一份加载路径,但会话状态互相独立。在图形界面里setSkillPath加的路径,不会带到命令行解释器里,重启就没了,所以能写进启动文件的配置就不要靠手敲。

3. 把中文指南挂进工作流:目录、术语与第一个可加载文件

文档下载下来只是第一步,真正影响效率的是它能不能被快速检索、术语能不能和代码对上。机器翻译的版本在这一点上尤其需要预处理,否则读到第三章就会开始怀疑自己。

3.1 目录怎么摆:文档、示例、笔记三分离

我给自己定的规矩是三类东西分三个目录放,谁也别混在一起。混放的后果是半年后回来,分不清哪个.il是从文档里抄的示例,哪个是自己改过的版本。

# 目录骨架,一条命令建完 mkdir -p $HOME/skill/{doc,lib,lab,notes} # doc —— 放中文指南和原版对照,只读,不改 # lib —— 放稳定可复用的 .il 源码,会被加载路径引用 # lab —— 放试验性代码,随时可删,不进加载路径 # notes—— 放术语对照表和踩坑记录

doc只读这一点要严格执行。中文指南是机器翻译的,读的时候一定会有想改错别字、想调整语序的冲动,一旦动手改,后面就说不清「文档原文是什么」和「我理解的是什么」了,排查术语歧义时这层区分很关键。lib和lab分开的意义在于加载路径只指向lib,试验代码放在lab里即使有语法错误,也不会在启动时被自动加载导致整个环境起不来——这个坑我踩过一次,重启图形界面三次才反应过来是自己昨天写的半截文件被自动加载了。

命名上我用「章节号 + 主题」的方式,比如ch04-lists.il、ch06-control.il,跟文档的章节顺序对应。这样翻文档和翻代码是同一个心理路径,找东西快。

3.2 机器翻译版的术语对照表,先建后用

机器翻译的文档最大的问题不是句子不通,是同一个英文术语在不同章节被译成了不同的中文词。比如描述列表结构的几个词,有的章节译成「头」和「尾」,有的直接音译或者意译成别的说法,读到后面根本对不上代码里的函数名。我的处理方式是边读边建对照表,读到哪个术语就补一行,不追求一次建全。

中文译法(文档中出现)英文原词对应函数或概念常见误读
清单 / 列表listlist、cons误以为是数组,按下标访问
头 / 首元素carcar以为返回子列表
尾 / 剩余部分cdrcdr以为返回最后一个元素
过程procedureprocedure与lambda混用
语法函数syntax functiondefmacro一类以为和普通函数等价
局部变量local variablelet、prog以为出了作用域仍可读写
原子atomatom与符号symbol混淆

这张表我只记真正卡住过我的条目,不追求完整。它的用法很直接:读文档看到某个中文词拿不准,回来查表确认英文原词,再去代码里搜对应的函数名,对照函数的行为验证。这样即使译文有偏差,也能靠函数名这个锚点锚住。

注意:涉及求值时机、作用域、参数传递这三类概念时,不要只信中文译法,一定要找到对应的英文原词。这三块是翻译偏差最集中的地方,也是最容易写出「语法通过、结果不对」的代码的地方。

3.3 写第一个能 load 的 .il 文件并跑出返回值

到这一步环境已经具备,该验证一条完整的链路:文件存在、路径可达、加载成功、函数可调用、返回值正确。

; $HOME/skill/lib/hello.il ; 最小可加载单元:定义函数、处理参数、返回字符串 procedure( MYhello( name ) let( ( greeting ) ; strcat 做字符串拼接,第二个参数必须也是字符串 greeting = strcat( "hello, " name ) ; printf 输出到当前输出端口,%s 表示字符串占位 printf( "MYhello: %s\n" greeting ) ; let 的返回值是最后一个表达式的值,这里就是 greeting greeting ) )

这段代码有四个点值得说。procedure定义的是普通函数,参数在传入前已经被求值,所以name拿到的是值而不是表达式。let用来声明局部变量,第一层括号里是变量名列表,即使只有一个变量也必须用双层括号包住,写成let( greeting ...)会直接报语法错,这是新手最常写错的一行。strcat只接受字符串,传入数字不会自动转换,得先用sprintf格式化。最后那个裸的greeting不是多余,SKILL 的函数返回值就是函数体最后一个表达式的值,不写这一行函数会返回printf的结果。

; 加载并调用 load( strcat( getShellEnvVar("MY_SKILL_LIB") "/hello.il" ) ) MYhello( "skill" )

load的参数是完整路径,这里用strcat拼了环境变量,避免写死。加载成功会返回一个非nil的值(通常是文件路径或者t),如果返回nil或者报找不到文件,问题一定在路径上,先echo $MY_SKILL_LIB确认变量,再ls确认文件,两步就能定位。调用MYhello("skill")应该打印MYhello: hello, skill并返回字符串"hello, skill"。

函数名前缀MY是我自己的习惯,取自文档里「为你的函数选择前缀」那一节的建议。前缀的作用是避免和工具自带的函数重名——SKILL 允许重定义,一旦重名,你会在毫无提示的情况下覆盖掉内置函数,后面所有依赖它的代码全部行为异常,排查成本极高。前缀用两三个大写字母就够,别用太长。

4. 按文档写代码:函数形态、参数修饰与作用域

环境跑通之后,真正的成本在写代码这一侧。文档从第三章开始讲函数定义,那一章信息密度最高,也是机器翻译偏差最集中的部分,我按自己实际用得上的顺序重新组织了一遍。

4.1 六种定义函数的语法,按用途挑

文档列了procedure、lambda、nprocedure、defmacro、mprocedure、defglofun六种定义方式,名字看着唬人,实际日常写代码九成以上只用第一种。剩下的几种存在感低,但遇到对应场景必须用对,用错不会报错,只会结果诡异。

; 1) procedure:最常用,参数先求值后传入 procedure( MYadd( a b ) a + b ) ; 2) lambda:匿名函数,通常直接作为参数传给别的函数 mapcar( lambda( (x) x * 2 ) list( 1 2 3 ) ) ; 3) nprocedure:参数不求值,直接以表达式形式接收 nprocedure( MYshow( expr ) printf( "raw: %L\n" expr ) )

procedure是标准选择,参数求值后再进函数体,符合大多数人的直觉。lambda用在只需要一次性使用的地方,比如给mapcar传个变换规则,定义完就丢,不占命名空间。nprocedure是「不求职」的版本,参数传进来的是原始表达式本身而不是它的值,只在需要自己控制求值时机时才用——比如写一个延迟执行或者条件执行的包装。文档里nprocedure和mprocedure的译名容易看混,记住前者是参数不求值,后者跟宏展开相关,用途完全不同。

defmacro是语法层面的展开,它在代码被求值之前就完成替换,所以宏的参数天然不求值,返回的是一段待求值的代码而不是值。这一点如果理解成「普通函数」去写,最典型的表现是:宏里对参数做了运算,结果发现参数被求值了两次或者根本没求值。日常写脚本很少需要宏,只有在要构造一套自己的语法糖时才用得上,我的建议是前半年先别碰。

defglofun这类全局函数定义方式在文档里占的篇幅很小,实际项目里也少见,遇到时按文档示例照抄即可,不必深究。

4.2 参数修饰符:@rest、@optional、@key 的取舍

参数修饰符是 SKILL 相对其他脚本语言比较有特点的部分,也是写工具函数时最实用的一个特性。三个修饰符解决三个不同的问题:变长参数、可选参数、关键字参数。

; @rest:收集剩余的全部参数为一个列表 procedure( MYsum( @rest nums ) let( ( total ) total = 0 foreach( n nums total = total + n ) total ) ) ; 调用:MYsum( 1 2 3 4 ) 返回 10

@rest后面只能跟一个变量名,它会把调用时多出来的所有实参打包成一个列表。上面的例子先用let把累加器初始化为0,注意必须是0而不是留空——留空默认是nil,nil + 1会直接报类型错误,这是文档「初始化局部变量为非空值」那一节反复强调的点,也是实际写循环时最常见的翻车点。

; @optional:可选参数,可以给默认值 procedure( MYbox( width height @optional (layer "M1") (scale 1.0) ) list( width height layer scale ) ) ; 调用:MYbox( 10 20 ) → (10 20 "M1" 1.0) ; 调用:MYbox( 10 20 "M2" ) → (10 20 "M2" 1.0)

@optional之后的参数按位置匹配,可以带默认值,写法是「变量名 默认值」包一层括号。调用时少传的参数取默认值,多传的直接报错。这里有个取舍:位置参数一旦超过三个,调用方就得靠记忆排顺序,可读性急剧下降,这时候该换@key。

; @key:关键字参数,调用时用 变量名: 值 的形式传 procedure( MYrect( @key width (height 10) (layer "M1") ) list( width height layer ) ) ; 调用:MYrect( ?width 20 ?layer "M2" ) → (20 10 "M2")

@key之后的参数在调用时通过?变量名 值的形式指定,顺序无关,不传的取默认值。工具函数里凡是参数超过四个的,我基本都用@key写,调用方一眼能看出每个值是什么含义,改起来也不怕顺序错位。代价是调用时略啰嗦,但这个成本远比调错参数顺序后排查半天低。

三种修饰符可以在同一个函数里组合使用,文档里叫「结合参数」,规则是@rest必须在前、@optional居中、@key收尾,顺序写反直接语法错。

4.3 let 与 prog:局部变量和全局变量的边界

作用域这块是我见过翻车最多的地方,因为它的失败方式很隐蔽:函数第一次调用结果正确,第二次调用结果就偏了,原因是上一轮的值留在了全局变量里。

; let:声明局部变量,出了函数体就不可见 procedure( MYarea( w h ) let( ( area ) area = w * h area ) ) ; prog:在 let 的基础上多了 return 能力,适合需要中途返回的循环 procedure( MYfind( target lst ) prog( ( rest ) rest = lst while( rest when( car( rest ) == target return( rest ) ; 立即返回,跳出整个 prog ) rest = cdr( rest ) ) nil ; 走完没找到,返回 nil ) )

let的语义很干脆:变量列表在这一层里有效,函数返回就销毁。它的问题是没办法中途退出,循环里找到目标还得靠标志位绕出来。prog补上了这一块,它有独立的return,在循环内部直接返回整个函数的结果。两者都要求变量初始化,不留空就默认nil,参与算术运算前必须赋值。

全局变量是另一个方向。SKILL 里不加let/prog包裹直接赋值的变量就是全局的,跨函数、跨会话都可见,图形界面重启之后某些还能留着。我的做法是全局变量统一加前缀集中声明,用defvar一类的方式在启动脚本里初始化,业务函数里一律只读。原因很实际:全局变量一旦被某个函数偷偷写坏,表现是另一个毫不相干的模块开始出错,排查时根本联想不到源头。

注意:调试期临时加个全局变量记状态很方便,但一定要在上线前清掉。它会成为黑匣子,问题是它自己不会报错。

4.4 控制流与列表操作的最小骨架

控制流部分文档讲得比较全,if、when、unless、case、for、foreach全都有,实际写代码时按下面这张表选就行,不用记语法细节。

场景用哪个关键点
两种分支,都要有返回值if返回被选中分支的值
一种分支,条件成立才做when条件不成立返回nil
一种分支,条件不成立才做unless与when互补
多值匹配case匹配项按常量比较
固定次数循环for起止值都包含在内
遍历列表foreach逐个取元素,不取下标
; 常用骨架:遍历列表并过滤 procedure( MYfilter( pred lst ) let( ( result ) result = nil foreach( item lst when( pred( item ) result = cons( item result ) ; 头插,结果会逆序 ) ) reverse( result ) ; 逆回来,保持原顺序 ) )

这段代码里cons把头插进去是 O(1) 的操作,比每次append到尾部再遍历一遍效率高得多,代价是顺序反了,最后用reverse补回来。foreach只遍历不返回,累加结果必须靠外部变量接住,这一点跟其他语言的map类函数不同,容易写错成「foreach的结果就是返回值」——它返回的是nil。

列表的访问和修改按文档那一节的说法就是围绕car、cdr、cons三个基础操作展开,其余函数都是组合。初学时建议只用这三个加reverse、length写代码,写顺了再去用现成的便利函数,理解会扎实很多。把上面这几个骨架存成模板放在lab目录里,新写函数时直接改,比每次从空白文件开始快得多。

5. 避坑与排查:装完跑不通的五个现场

这一节记的都是我自己撞过的,按现象、原因、解决三段写。其中前三条集中在加载阶段,后两条跟求值和作用域有关。

5.1 加载阶段的三类报错

现象一:load返回nil,没有任何其他提示。

原因基本只有两种,路径拼错或者文件不存在。SKILL 的load对找不到文件这件事非常安静,不像其他语言会抛异常,它就是不返回东西。

解决分三步确认:先echo环境变量看路径是否正确,再ls看文件是否真的在那个位置,最后在 SKILL 里用绝对路径load一次排除路径拼接问题。如果绝对路径能load成功、拼路径不行,问题就锁定在strcat拼接上——最常见的是环境变量末尾多了个/,拼出来变成双斜杠,或者少了个/拼成了libhello.il。

现象二:load成功,但调用函数报「未定义」。

原因通常是加载的文件里函数名拼写和调用时不一致,或者文件名里包含中文、空格导致加载被截断但没报错。

解决办法是把文件名和函数名都限制在 ASCII 字母、数字、下划线、短横线之内,目录路径也一样。中文路径在部分环境下能加载但行为不稳定,血泪经验是别去试。函数名统一加大写前缀之后,重名和拼写问题基本消失。

现象三:重启后一切正常,运行一段时间后load开始报错。

原因是文件被外部修改了,比如编辑器保存时换行符变了,或者文件里有未闭合的括号。SKILL 的解析器对括号极其敏感,一个左括号没配对上,报错位置往往指向文件末尾而不是真正出问题的那一行。

解决办法是先用交互式解释器单独load这个文件,看它给出的报错行号;然后从文件开头按段注释掉,二分定位问题段。文档里专门有一节讲「超右括号」,讲的就是多出来的右括号这种情况——它比缺括号更难找,因为解析可能在上一行就结束了,报错却落在下一行开头。养成写完一段就load一次的习惯,比攒一大段再调试省时间。

5.2 求值与作用域的两类错位

现象四:函数第一次调用结果正确,第二次调用结果偏移。

原因是用了全局变量做累加器,或者let的变量列表写漏了某个变量,那个变量就退化成了全局的。

解决方式是检查每个参与赋值的变量是否都在let或prog的变量列表里,一个不漏。判断方法是函数体外printf一下那个变量名,如果能看到值,说明它泄漏成了全局。修完之后在函数末尾把临时全局变量显式置nil,作为兜底。

现象五:明明传的是表达式,函数却拿到值,或者反之。

原因是对procedure和nprocedure、defmacro的求值时机理解反了。procedure的参数在传入前求值,nprocedure不求值,defmacro在展开阶段处理。三者混淆的典型表现是:想做延迟执行,用了procedure,结果参数在传入那一刻就被算了。

解决办法是只在这三种情况下用nprocedure:需要拿到原始表达式、需要控制求值次数、需要写自定义的控制结构。其余场合一律用procedure,写起来最不容易出问题。改的时候一并检查调用方,因为换定义方式会改变调用约定,两边必须同时改。

6. 验证跑通:三条自检和一份进度表

装完配完之后,「能跑」这个概念要落到可重复的检查上,否则下次换机器又是一轮重来。我固定跑三条自检,每条都有明确的通过标准。

# 自检一:可执行文件与环境变量 which virtuoso && echo "CDS_INST_DIR=$CDS_INST_DIR" && echo "MY_SKILL_LIB=$MY_SKILL_LIB"

通过标准是三个输出都有值且路径正确。这一步排掉的是 PATH 顺序错、变量没写入配置文件这类问题,它们会在此后所有步骤里持续制造噪声。

; 自检二:加载路径与源码加载 getSkillPath() load( strcat( getShellEnvVar("MY_SKILL_LIB") "/hello.il" ) ) MYhello( "check" )

第一行打印加载路径,确认个人目录在列表里;第二行返回非nil表示文件加载成功;第三行返回字符串"hello, check"表示函数定义和调用链路完整。三行里任何一行不符合预期,就回到上一节对应的条目排查,不要带着问题往下走。

# 自检三:批处理模式下完整跑一遍,确认非交互环境也正常 virtuoso -nograph -replay $MY_SKILL_LIB/check.il

check.il里放的是前面那些调用加上打印语句,作用是验证在无图形界面、无人工干预的情况下所有依赖都能正确解析。这一步能暴露那些依赖图形界面初始化才存在的隐式状态,很多人只在图形界面里测过,一上自动化流程就挂。

三条跑完,我把结果记在一张进度表里,跟着文档章节走,读完一节就补一行,避免「读过」和「会用」混为一谈。

阶段对应文档内容通过标准
环境可用前言、安装与环境基础设施三条自检全部通过
语法骨架开始、SKILL 清单能独立写出列表遍历与过滤
函数定义在 SKILL 中创建函数六种定义方式能说清用途差异
参数与作用域定义参数、局部变量、全局变量能判断一段代码里哪些变量是全局的
控制流控制流程、文件输入输出能把一段手写循环改成foreach

从那以后我每次搭这套环境,都会先跑一遍三条自检再开始写代码,不管这台机器是不是刚装过——省下来的时间远比检查花的多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询