feat: unify source and skills for Dever 0.1.2 installation

This commit is contained in:
Antigravity committed 2026-10-05 02:49:17 +00:00
1 parent a2e0436ed4
commit 19f122b4b4
47 files changed
+1951 -63

No files matched your search

Submodule skills deleted from 27ac8f21c2.
+2
View File
@@ -0,0 +1,2 @@
__pycache__/
*.pyc
+51
View File
@@ -0,0 +1,51 @@
---
name: dever-language
description: 开发、检查、测试或维护 Dever 语言应用(.dever、.dever.md),创建新项目,安装或更新 dever 命令,编写 App、Domain、Model、API、CMD、Job、Port/Adapter,接入 Dever Package 或 Python/JS/Go Lib 时使用。也用于 Dever 应用目录、权限、租户和命名约束检查。不要用于 Go Dever 框架、dever-go 或普通 Go 组件开发。
---
# Dever 语言开发
Dever 是独立语言。应用源码使用 `.dever` 或 `.dever.md`,不沿用 Go Dever 的 service、Page JSON 或 Provider 目录;第三方语言实现经 Port/Adapter 接入。
## 先确定版本和范围
1. 读取用户项目的 `AGENTS.md`、现有目录和 `config/setting.json`。回答、分析、诊断只做只读检查;用户要求修改时再实施,不擅自安装软件、改服务或改全局命令。
2. 若已安装机器版 Dever,新项目运行 `dever skill path`,已有项目运行 `dever skill path <project-root>`,读取返回目录中的 `SKILL.md` 与本次需要的参考文件。已经在那个目录读取本 skill 时不再递归加载。它由 `dever update` 与核心一起更新;已有项目按其可选 dever.version 选择对应指引,不能把活动版本新增语法强加给旧项目。
3. 尚无 `dever` 时按 [安装与更新](references/install.md) 引导安装;不能把 `dever-go` 当作 Dever 语言命令。公开发行包缺失时如实说明,不编造成功、不自动编译一套缺少 runtime 的开发二进制。
4. 新项目使用 `dever new <new-directory>`;Markdown 项目加 `--markdown`。不要手工猜模板,不覆盖已有目录。项目模板的用途和检查命令见 [项目开发](references/development.md)。
开发、重构或审查应用代码前,必须读取 [项目开发](references/development.md) 中的文件与函数组织、配置与硬编码、禁止绕过检查、验证范围。安装或纯语法问答只加载与请求相关的参考。
## 按需要阅读
| 工作 | 先读 |
| --- | --- |
| 新项目、目录、命名、测试与交付 | [development.md](references/development.md) |
| 类型、分句、函数、集合、失败和并发 | [language.md](references/language.md) |
| HTTP/CMD、Model、权限、租户、Job | [application.md](references/application.md) |
| 标准能力、外部生态、Package | [libraries.md](references/libraries.md) |
| Markdown 源码 | [markdown.md](references/markdown.md) |
| 安装命令、安装 skill、更新 | [install.md](references/install.md) |
只加载当前任务相关参考。当前版本的编译器检查和已验证模板决定语法;参考中没有的调用先查官方对应版本源码,不能按其它语言习惯杜撰方法。
## 必须遵守的应用边界
- 配置只在 `config/setting.json`;不通过环境变量读取配置、密码、数据库 URL 或选择工具链。不要把 Dever 的官方仓库地址、更新配置写入业务项目。
- 应用目录是 `module/<component>/<domain>/<role>.dever`;role 只有 `app/domain/model/port/adapter/api/job`。复杂职责再按主题拆分,不机械地每个函数建一个文件,不建 `common/utils/helper/base/service` 杂物目录。
- 不写 `package/import/exposes/main`。应用入口由 API、CMD、Job 声明生成。App 是对外领域能力;跨领域只调用目标 App,不直接调用其私有 Model、Domain、Adapter。
- API 只声明绑定或 REST,App 编排,Domain 保存内聚的业务规则,Model 声明持久化合同,Port/Adapter 隔离外部 I/O。已有标准能力直接复用,禁止重复手写 ID 解析、分页裁剪、JSON 拼装等碎片函数。
- 函数使用命名输出和互斥、穷尽的相邻分句;没有 `if/else/switch/match/return`、任意循环或递归。不要靠分句顺序隐藏重叠规则。
- HTTP 默认有权限,匿名入口才写 `public`。不添加 `auth.require`、客户端自报身份/权限或每个 API 重复鉴权。站点按配置里的 API 目录匹配,租户按数据库隔离。
- 测试放在 `test/<component>/<domain>/<topic>.dever`,入口为文件同名 `topic() ()`。外部调用使用 case-local Port fake,不在普通测试启动真实 Lib 或访问真实业务数据库。
- `run/build` 离线。依赖准备使用显式 `dever package` / `dever lib` 命令;已有锁在新机器用 `dever lib install`,ARM 应用目标用 `dever target add linux-aarch64`。这些命令按需准备机器共享资源,不扫描宿主 Python、Node、Go 或 PATH 来充当运行环境。
## 工作方式
1. 修改前查相关领域、实际调用链、标准能力和既有实现。对非平凡改动简要说明缺少什么行为、应该由哪一层负责、哪些文件需要改变;已有实现满足要求时保留它。
2. 实施时以完整业务职责组织文件和函数。抽取须有业务含义、明确不变量、实际复用或降低复杂流程理解成本的作用;不按行数或函数个数机械拆分。只清理本次引入或废弃的重复与死代码,保留无关改动。
3. 完成后审查新增调用链、公开合同、配置与可信身份来源,检查本次引入的诊断并执行范围内验证。禁止假调用、吞错、假成功、擅自修改接口基线或弱化测试来通过检查;详见开发指引。
`fmt --check`、`check`、`test` 和 `run/build` 的覆盖范围不同。`check` 与 `run/build` 共用应用入口可达性和冗余硬检查,保持静态、离线,不执行应用;具体见 [验证范围](references/development.md#验证范围)。`dever test` 没有筛选参数,已有大项目不擅自全量测试;新模板的小套件可完整验证。已有授权覆盖的验证直接执行,启动服务、接真实数据库或更改系统安装须遵守用户的范围限制。
交付说明实际改动、验证结果与未验证边界。规则写入 skill 不等于新增编译器检查,能编译不等于业务验收、性能验收或已发布。
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "Dever 语言开发"
short_description: "安装与更新 Dever,创建项目,按语言约束开发、检查和测试应用"
default_prompt: "使用 $dever-language 创建并验证一个符合当前语言规范的 Dever 项目。"
+1
View File
@@ -0,0 +1 @@
7889e0edaf5fbf1060bef4ad6e27106128485c8412a6f6cd4457dc67e112986e
+2
View File
@@ -0,0 +1,2 @@
/data/
/hello-app
+14
View File
@@ -0,0 +1,14 @@
# Dever 应用开发
在本项目运行 `dever skill path .`,读取返回目录的 `SKILL.md` 和 `references/development.md`,使用与项目版本匹配的规则。AI 尚未接入 skill 时,可用 `dever skill install <新的AI技能目录>/dever-language` 创建稳定入口。这里使用 Dever 语言,不是 Go Dever 框架;命令缺失时按 README 的安装入口处理,不猜语法或用 dever-go 替代。
- 源码放在 `module/<组件>/<领域>/`,按职责使用 `app`、`domain`、`model`、`api`、`port`、`adapter`、`job`;只创建实际需要的角色。
- 不写 `main.dever`、`package`、`import` 或 `expose`。API/CMD/Job 是入口;跨领域调用 App。
- 部署配置只用 `config/setting.json`,不增加环境变量配置或写死真实地址、凭据。用户、租户和权限来自可信上下文,不用特殊账号 ID 判断权限;正常字面量与明确业务常量可以保留。
- 修改前先读实际调用链并查找复用点。文件按内聚业务主题组织,函数须有业务含义、不变量、实际复用或隔离复杂流程的作用;不按行数拆分,不机械地一个函数一个文件,不建 `utils/common` 等通用桶。
- 普通 CRUD 用 Model 与 REST,增加业务合同或编排才写 App,不叠加空转发层。API 只声明入口,Domain 保存业务规则,外部能力经 Port/Adapter。
- 不用假调用、吞错、假成功、修改接口基线、弱化测试或转移到第三方语言绕过检查。只清理本次引入或废弃的代码,保留无关修改。
- 保持当前项目源码格式一致。Markdown 源码只编译顶层 `dever` 围栏,声明文档必须与源码一致。
- 修改后执行范围内的 `dever fmt . --check`、`dever check .` 和适当验证。check 静态执行与 run/build 相同的应用入口可达性及冗余硬检查,不准备外部依赖或执行应用;报告实际覆盖范围,不能把 check 成功当作全部验收。
- 普通测试位于 `test/<组件>/<领域>/`,用文件同名零输入零输出函数,外部能力用 Port fake。`dever test .` 会运行全部应用测试,已有大项目不默认全量执行;新模板的小套件可完整验证。遵守用户对生产数据、服务、真实数据库和压测的限制。
- 安装、升级、依赖准备与业务运行分开;`run/build` 不隐式联网。没有用户要求,不提交或推送 Git。
+24
View File
@@ -0,0 +1,24 @@
# Dever 应用
这是无需数据库、鉴权或第三方工具的最小 CMD 项目。
在项目目录运行:
```sh
dever check .
dever fmt . --check
dever test .
dever run . -- hello.greeting.greet '{"name":"Dever"}'
dever build . --output hello-app
./hello-app hello.greeting.greet '{"name":"Dever"}'
```
问候结果为 `Hello, Dever`。输入由 `api` 的 CMD 声明解析并传给 `app.greet`;业务不读取环境变量,配置在 `config/setting.json`。
以上命令适用于这个最小模板;项目扩大后按改动选择验证,`dever test .` 会执行整个项目的应用套件。`check` 静态执行与 run/build 相同的应用入口可达性及冗余硬检查,无需准备运行资源;它不执行应用,不能代替运行或业务验收。
AI 开发规范见本项目 `AGENTS.md`。运行 `dever skill path .` 获取项目对应版本的完整 `dever-language` skill,开发前读取其中 `references/development.md`。AI 的稳定 skill 入口可用 `dever skill install <新的AI技能目录>/dever-language` 安装;核心与配套 skill 通过 `dever update` 同步更新,已锁定版本的项目仍使用自己的版本。
首次安装入口为 `https://git.dever.cc/shemic/dever/install`,源码仓库为 `ssh://git@git.dever.cc:2222/shemic/dever.git`。安装前确认该站已发布对应平台的脚本和签名包;部署完成后可用 `curl -fsSL https://git.dever.cc/shemic/dever/install | bash`,机器安装需要管理员权限。仓库可访问不代表二进制已经发布。
普通 `.dever` 与 Markdown `.dever.md` 共享语言规则。用 `dever new <新目录> --markdown` 创建 Markdown 项目。目录必须不存在,生成器不会覆盖已有项目。
@@ -0,0 +1,5 @@
{
"log": {
"level": "info"
}
}
@@ -0,0 +1 @@
cmd greet = app.greet
@@ -0,0 +1,17 @@
# 问候命令
将命令行 JSON 输入交给 App。
- 包:`hello.greeting.api`
- 公开类型:无
- 公开方法:无
- 使用:无
## greet
声明问候命令。
- 声明:`cmd greet`
```dever
{{source}}```
@@ -0,0 +1,3 @@
greet(name: Text) (message: Text) {
message = "Hello, " + name
}
@@ -0,0 +1,23 @@
# 问候
提供可复用的问候用例。
- 包:`hello.greeting.app`
- 公开类型:无
- 公开方法:
- `greet`
- 使用:
- `hello.greeting.greet(name)`
## greet
根据名字生成问候。
- 函数:`greet`
- 输入:
- `name: Text`:名字
- 输出:
- `message: Text`:问候
```dever
{{source}}```
@@ -0,0 +1,4 @@
greet() () {
message = app.greet("Dever")
assert_eq(message, "Hello, Dever")
}
@@ -0,0 +1,19 @@
# 问候测试
检查问候用例的输出。
- 包:`hello.greeting.greet`
- 公开类型:无
- 公开方法:无
- 使用:无
## greet
调用真实 App 并断言输出。
- 函数:`greet`
- 输入:无
- 输出:无
```dever
{{source}}```
+67
View File
@@ -0,0 +1,67 @@
# 应用接口、数据与后台任务
## API 和 CMD
`module/news/article/api/admin/manage.dever`:
```dever
get list = app.list
get detail = app.detail
post create = app.create
put replace = app.replace
delete remove = app.remove
post publish = app.publish
```
API 无函数体,继承 App 输入/输出签名。支持 get/post/put/delete,当前没有 patch、旧 get_ 前缀或源码内 site 声明。`cmd greet = app.greet` 生成 `component.domain.greet` 命令,参数来自单个 JSON 对象;main.dever 不存在,也不读取命令行参数。
GET/DELETE 从 query 读标量参数,POST/PUT 从 JSON body 读参数;额外、重复或缺少必填字段均拒绝。无需自己读 body 或构造 dever.http.Response。成功统一为 `{"code":0,"message":"ok","data":...}`;错误 data 为 null,code 对应状态码。接口必须绑定一个可序列化输出,输出名可自定。
`dever.api.Error` 中 Invalid/Unauthorized/Forbidden/NotFound/Conflict/TooManyRequests 对应 400/401/403/404/409/429;未知内部失败记录脱敏日志并返回安全的 500。
写 HTTP/CMD/Job 调用链由入口建立事务;GET 调用链不能写库或入队。密码 hash/verify 是例外:不能让昂贵密码运算占用入口长事务,实际写入必须放显式短 transaction,并在其中重新检查依赖状态。不能为了“默认有事务”而删除这种短事务。
## 站点、身份与权限
`sites.admin.path = "admin"` 匹配领域内 `api/admin/**`,front 同理。文件名 topic 也进入 URL;上述 publish 是 `/news/article/admin/manage/publish`。每条 HTTP 声明只能匹配一个站点,重叠或遗漏配置会失败;path 不改写 URL,也不匹配 `api/admin.dever`。
所有 HTTP 默认鉴权并校验精确权限。只有明确匿名操作写 `public post login = app.login` 等单条 public 声明;没有 public rest、public cmd、auth.require 或每个业务文件的权限注册代码。
配置的 auth provider 引用一个项目自己的 `verify` App 方法,验证账号、会话、成员和租户关系。输入是 `dever.auth.Claims`,输出 record 必须且只能包含 `id: Text`、`user_id: <UserModelId>?`、`tenant_id: <TenantModelId>?`;只能读 global Model,不能写库、拿原始 Header 或返回自报权限。
认证成功的受保护 App 可读取 `dever.auth.id()`、`session()`、`user_id()`、`tenant_id()`、`dever.site.key()`;来源是隐藏可信上下文,不能让客户端提交 user_id/tenant_id 当作身份。名义 ID 必须与站点 verify 的类型对应,可空 ID 经分句处理后才能使用。public/CMD/Job/普通测试不会自动获得身份读取能力。
核心生成 `component.domain.site.action` 权限目录,method 是元数据,不额外拼进权限键。REST 的 action 固定为 read/create/replace/delete。核心维护角色、用户角色、角色权限;项目无需重建这些表。App 用 `dever.auth.permissions/save_role/grant_role/revoke_role/disable_role` 实现后台授权入口,具体参数查当前库签名。它们只能从受保护写 API 到达。资源所有权可用 `dever.auth.owns_user(id)`,不等同于接口角色权限。
Cookie 登录/退出用 `dever.auth.issue_cookie(subject, session, tenant)` 与 `clear_cookie()`。Header/Cookie 使用 `dever.api` 读取/设置,无需显式 context 参数。密钥来自部署配置;Cookie 写入和 Cookie 认证的写请求需要精确同源校验,站点配置 origin。敏感头不能当普通 Text 读取。
## Model、CRUD 与业务编排
`module/news/article/model.dever` 的主记录应为 Article:
```dever
type Article {
title: Text(1, 200)
body: Text
}
```
自动有只读 `id` 与 `created_at`。字段支持 default、private、unique、index;范围和长度在存储边界验证。关系使用目标 Model 专有 id 字段,不用 Int 代替外键。
同领域 App 可用 `model.create({...})`、`model.get(id)`、`model.update(id,{...})`、`model.delete(id)`、`model.list({where=...,order=[created_at.desc,id.desc],page=1,size=20})`。这是编译器静态 DSL,不是动态字典 SQL;不用手写 repository。聚合、关联和插入流程仍由 App 明确编排,不能自动推测关联图级联写入。
纯粹 CRUD 可在 API 写 `rest model`,多 Model 时指定 `rest model.<topic>`。Model 中 `create = expr`、`replace = expr`、`search = expr` 控制 REST 字段来源/查询;引用 input 表示客户端同名输入转换,不引用 input 表示服务端提供且禁止客户端提交。不要改成 post/put/get 字段关键字。
`private owner user_id: ... = dever.auth.user_id()` 在创建时赋值,并将所有权条件加入全部读写 SQL;不是只在输出隐藏 user_id。这种静态 owner 隔离可由 Model 直接声明,无需仅为它增加 App。关联操作、超出字段合同的业务校验、复杂输入输出或发布状态机由 App 编排,不能藏在 API 里。App 对外用明确 View,别泄漏 Model 私有字段。
不为每个 CRUD 机械建立 App、Domain、Port 和 Adapter;Model 的数据库操作无需再套一层 Port/repository。实际新增业务合同或编排时才增加对应实现。完整反重复、反硬编码及检查边界见 [项目开发](development.md)。
## 租户、任务与部署配置
配置只有 database 租户隔离:`tenant.database` 选控制连接,`global type` 使用平台库,其余持久 Model 与 Job 使用租户库。没有 field/table/mode 配置。租户缺失、未迁移或 schema 过期明确失败,不回退平台库。
运维命令:`dever tenant migrate <root> <id>`;`dever tenant owner <root> <id> <site> <user-id>` 建立核心 Owner;`dever tenant component enable|disable <root> <id> <component>` 控制租户功能。可用租户组件从 Model/Job 推导,不手写 Profile 分组。
job.dever 声明持久任务并绑定 App;enqueue 与事务、重试、调度、权限重验使用核心机制,不另写后台死循环。实际 Job 声明从对应版本指南和 CMS publishing/schedule 示例复用,不能把第三方 Worker 当持久 Job。
部署配置的真实结构可查官方 CMS `config/setting.json`,只添加项目用到的 database、tenant、auth、sites、http、job、runtime、log。最小 CMD 项目只需模板配置。密码、token 与真实数据库连接不提交 Git。
+128
View File
@@ -0,0 +1,128 @@
# 项目开发
新建最小项目:
```sh
dever new my_app
dever fmt my_app --check
dever check my_app
dever test my_app
dever run my_app -- hello.greeting.greet '{"name":"Dever"}'
dever build my_app --output my_app/program
./my_app/program hello.greeting.greet '{"name":"Dever"}'
```
创建 Markdown 项目用 `dever new my_app --markdown`。两种模板使用相同代码和行为,Markdown 另加编译器校验的说明。目标目录必须不存在,父目录必须已存在;不接受向现有项目合并或覆盖。build 输出也必须不存在。
模板只包含 CMD、App、配置、一个测试和开发指引,无需数据库或 HTTP 服务。实际项目在它的目录合同上扩展,替换演示领域,删除不再使用的示例与测试。添加 HTTP 时须完成 [站点与身份配置](application.md),不能只把 cmd 换成 get。
```text
my_app/
AGENTS.md
README.md
config/setting.json
module/
news/article/
app.dever
domain.dever
model.dever
port.dever
adapter.dever
job.dever
api/
admin/manage.dever
front/browse.dever
test/news/article/publish.dever
```
这是可用职责位置,不是要求创建全部文件。component、domain 和文件主题使用有业务意义的 ASCII 小写蛇形命名;类型用 PascalCase,方法和字段用 snake_case。不要增加任意第三层领域目录。组件按业务能力划分,领域按一致的数据和业务边界划分。组件、领域和主题不能命名为 `common/shared/utils/helper/helpers/base/service/services` 等编译器禁止的通用桶。
| 位置 | 责任和访问规则 |
| --- | --- |
| App | 跨领域公开能力,校验与业务编排;同领域用 `app.publish`,外部用 `news.article.publish` |
| Domain | 同领域内部业务规则;纯计算优先放这里,不承接数据库/外部 I/O |
| Model | 存储字段、索引、关系、迁移、Seed、静态 SQL;同领域 App 使用 ORM |
| Port | 外部能力的类型、操作与失败合同 |
| Adapter | 实现 Port;私有,不反调 App/Domain/Model |
| API | HTTP/CMD 入口绑定与自动 REST,无业务函数体 |
| Job | 持久任务声明,绑定同领域 App |
| Test | 文件同名、零输入零输出的唯一测试入口 |
## 文件与函数组织
简单领域保留角色单文件;复杂角色可以拆为 `app/publish.dever`、`app/search.dever` 等内聚主题,调用名不带文件名。非 API topic 目录不递归。App/Domain/Port/Adapter/Job 单文件与同名目录二选一,选择目录时至少有两个内聚主题文件;Model/API 主文件与 topic 目录可按编译器合同共存,不受这个最少文件数限制。只有 `api/` 可按站点和资源继续嵌套,目录会影响 URL。
一个文件可以包含多个相关类型、函数和分句。按完整业务主题命名,不机械地一个函数一个文件,也不为尚不存在的需求创建空 App、Domain、Port 或 Adapter。
抽取函数应表达独立业务规则、维护明确的不变量、复用已有逻辑,或隔离难以理解的复杂流程。只使用一次的函数也可以有这些作用;不能只因几行代码能起名字就再加一层。职责内聚的代码不按行数强拆,也不通过把所有逻辑塞进一个长函数来规避碎片化。
| 情况 | 处理方式 |
| --- | --- |
| `parse_id` 重新手写数字解析并把失败悄悄变成 0 | 复用已有解析、名义 ID 和明确失败合同,不复制算法 |
| `trim_name` 只原样转发一次标准 trim,没有新增合同 | 在使用处调用标准能力 |
| `page_slice` 重复 Model 已有的分页规则 | 使用 Model 分页,不在各领域复制一份 |
| `publish` 组织发布条件、状态变化和后续任务 | 保留完整 App 能力,把内聚业务规则放同领域 Domain |
| 单次使用的规则需要独立表达有效期或状态不变量 | 可以抽取并按业务含义命名 |
| 两处相似代码承担不同规则、未来变化原因不同 | 保持各自归属,不为表面相似增加通用调度层 |
相同规则先复用现有所有者;只有真实共享的逻辑才抽象。跨领域共享能力由负责该业务的 App 提供,不直接调用对方私有实现,不为缩短调用名建立空转发链。普通 CRUD 直接用 Model 和 `rest model`;需要超出 Model 字段合同的输入转换、业务校验、关联操作或输出合同才增加 App 编排,具体见 [应用规则](application.md#modelcrud-与业务编排)。
## 配置与硬编码
| 数据或规则 | 归属 |
| --- | --- |
| 随部署变化的服务地址、端口、数据库连接、凭据 | `config/setting.json`,不增加环境变量配置,不提交真实秘密 |
| 当前用户、租户、权限及资源归属 | 可信认证上下文、核心授权与 Model 策略,不接受客户端自报身份 |
| 有固定业务含义的状态、范围和规则 | 类型、互斥分句、字段约束或内聚 Domain 定义,复用同一规则 |
| 需要用户或运营维护的业务数据 | 项目 Model 与相应业务流程,不把具体记录写进源码分支 |
| 测试 ID、样例地址和固定结果 | 测试夹具,不作为生产失败时的兜底 |
禁止按某个账号、租户、记录的裸 ID、名字或地址开特例,例如用 `user_id == 1` 代替管理员授权。普通查询参数可以标识目标记录,但不能因此成为操作者身份或访问权限。
避免硬编码不等于搬走所有字面量。局部的 0、1、提示文字和稳定协议常量可以保留;有业务意义、难以理解或反复出现的值由现有类型或领域定义表达。只有确需运行时变化的值才增加配置。官方发行源是工具链固定合同,不能塞进业务 setting.json。
## 禁止绕过检查
- 不添加假入口、无意义调用或仅供测试触达的调用,掩盖生产入口不可达的 App/Domain。删除本次废弃代码或接通真实业务调用链。
- 不把重复业务逻辑转移到 Adapter、Python、JS 或 Go 来避开 Dever 检查。外部实现的质量同样需要审查。
- 不吞掉失败、用空值或假数据冒充成功;有业务依据的恢复须使用明确结果合同,并验证恢复行为。
- 不为通过检查擅自改 `dever.api`、删失败测试、弱化断言或关闭权限、类型及沙箱限制。确需改变公开合同,先说明调用方影响,在用户授权范围内同步实现、基线与测试。
- 普通应用开发不修改编译器或核心库来容纳违规代码;语言或核心维护须有相应任务授权。只处理当前改动相关问题,不借机重写无关代码。
## 测试与检查
`test/hello/greeting/greet.dever`:
```dever
greet() () {
assert_eq(hello.greeting.greet("Dever"), "Hello, Dever")
}
```
模板中的实际字符串以生成文件为准。测试只执行文件同名函数,不需要 main 或注解。`assert` 接受 Bool,`assert_eq` 要求同类型。测试可访问同领域 App/Domain,跨领域仅 App,不能直接写 Model CRUD。使用 Model 时运行器使用临时 SQLite;不使用 Model 时不需要数据库。它不会读取生产 setting.json 或生产 data。对业务失败使用 `result(call)` 配合 choice 分句验证。
常用命令:`fmt --check` 查格式,`check` 查语法/类型/目录/分句/配置,`api` 输出 API 合同,`test` 运行应用套件,`run` 开发执行,`build` 生成独立程序。不要发明 `test --filter`、`run api` 或 `dever init`。
### 验证范围
| 检查 | 能证明的范围 |
| --- | --- |
| `dever fmt <root> --check` | 源码格式;不证明类型或业务正确 |
| `dever check <root>` | 静态语法、类型、目录、命名、分句、相关配置、已有 API 基线,以及应用入口可达性和冗余硬检查;不执行应用 |
| `dever test <root>` | 当前项目全部应用测试的实际断言;没有筛选参数,不证明生产入口完整 |
| `dever run/build` | 通过与 check 相同的应用硬检查,再准备本地编译资源并执行或编译;成功不等于部署或性能验收 |
| AI 审查 | 业务含义、职责拆分、复用、硬编码与验证范围,不能替代程序检查 |
`check/run/build` 共用 C016,拒绝生产入口不可达的 App/Domain,以及符合现有判定条件的私有空转发和 Model CRUD 包装;并非任意角色、所有函数或全部 CRUD 包装都能检出。`check` 不准备 Worker 或 runtime pack,不进行原生编译,不启动应用、服务或数据库。`test` 保持独立的测试入口与 Port fake 检查,不执行生产入口可达性检查;不能用测试调用掩盖生产死代码,也不能把静态检查当作运行验收。
W001–W005 等诊断包含未使用赋值、部分冗余条件、相同私有实现、重复纯调用和空转发建议,警告本身不会使命令失败。审查本次引入的诊断,修正有证据的问题;不能声称编译器能判定全部硬编码或业务价值。skill 中的开发要求不自动变成编译器硬错误。
选择与改动相符且已获授权的检查。新模板的小套件可以完整运行;已有大项目不默认运行全量 `dever test`,也不默认启动服务、连接真实数据库或压测。需要但尚未运行的检查要说明原因和剩余证据,不能伪报通过。已有 `dever.api` 时保留基线约束;`dever api <root> --output <新文件>` 可以导出待比较合同,不覆盖原文件。
源码放 module/,测试放 test/,部署配置放 config/,运行数据放 data/。生成程序从二进制同级 config/setting.json 读取配置。应用无需打包编译器;使用外部 Lib 时相关 runtime/依赖由 build 嵌入。只读 check 不证明真实数据库或部署验收。
## 核心与业务的区别
统一源码仓库地址为 `ssh://git@git.dever.cc:2222/shemic/dever.git`,迁移与发行状态见 [安装指引](install.md)。`crates/dever-core` 负责语言检查/编译,`crates/dever-runtime` 负责运行时,`crates/dever-cli` 负责命令,`library/` 是随版本提供的标准 Dever 源码。维护中的完整业务示例是 `examples/cms/dever` 与 `examples/cms/md`;`examples/old` 是历史回归,不作新项目模板。
开发普通应用无需复制或修改这些核心目录。可复用业务组件作为 Dever Package;外部 Python/JS/Go 生态称 Lib。多个项目共享机器安装的 Dever。
+79
View File
@@ -0,0 +1,79 @@
# 安装与更新
安装入口为 `https://git.dever.cc/shemic/dever/install`,官方二进制源为同站 `/shemic/dever/dist`。这些地址由发行工具固定,不写入业务项目配置,不通过环境变量选择。Git 源码可访问不代表安装端点或二进制已发布;服务器尚未部署时如实报告,不能把登录 HTML 当作脚本。
## 安装 dever
先检查 `dever --help`。机器版提供 new、install、update、target、version、skill;仅源码开发二进制不管理机器安装。不要把已有同名命令直接删掉或覆盖,先识别归属。
初次安装需要 Linux、Bash、Python 3.9 或更新版本的 `/usr/bin/python3` 和支持 Ed25519 的 `/usr/bin/openssl`;管道下载还需要 curl。非 root 安装通过 `/usr/bin/sudo` 提权,帮助不提权。这些工具用于校验和安装,生成的应用不依赖系统 Python、Node、Go 或 Rust。检查安装脚本并取得机器安装授权后执行:
还须满足所选发行包的主机 ABI 要求;当前准备的 Linux x86_64 编译器要求 glibc 2.39。ARM64 应用交叉构建可在 x86_64 主机使用,不能据此宣称 ARM64 宿主编译器包已经发布。
```sh
curl -fsSL https://git.dever.cc/shemic/dever/install | bash
```
无需先克隆源码或安装 skill。查看参数用 `bash -s -- --help`;固定版本用 `bash -s -- --version <准确版本>`,替换上面命令尾部的 `bash` 即可。本地已下载脚本可用 `bash install --help` 检查。
该命令下载当前平台最新已签名基础包。脚本包含独立固定公钥,先校验发行元数据,再验证引导程序及其私有库摘要,由引导程序解压 Zstandard 包;无需系统 zstd。安装 `/opt/dever`、`/usr/local/bin/dever`、机器共享服务和必要沙箱策略,默认启动服务。`--no-start` 可仅安装。没有对应正式资产时明确失败,不修改已安装版本。
基础包包含 Dever 编译器、本机应用编译资源、四种数据库运行库和配套 skill。Python、Node、Go、第三方原生构建工具与 ARM 应用目标按需安装,保存在机器共享资源目录;同一版本的多个用户和项目共用一份。
## 将 skill 接入 AI
安装核心后,将稳定入口装到用户选择的 AI 技能目录:
```sh
dever skill install <新的AI技能目录>/dever-language
```
目录必须尚不存在,不能覆盖已有 skill。入口每次解析与核心配套的完整 skill,重载 AI 工具的技能列表后使用。这是 Dever 语言的 `dever-language`,不是 Go 框架的 `shemic-dever`。不自动修改机器上其他用户的 AI 目录。
## 按需准备资源
首次添加或更新 Lib 时,`dever lib add/update <project-root> ...` 自动准备相应生态,只有进入源码构建流程时才准备构建工具。已有 `dever.lock` 的项目在新机器运行 `dever lib install <project-root>`,按锁恢复依赖,保持配置和锁文件不变;没有 Lib 的 Python/JS/Go Adapter 也可通过该命令准备环境。缺少锁时先显式 `dever lib update <project-root>` 生成锁。不要把 install 当作重新选版本的 update。
在 x86_64 机器准备 ARM64 应用目标:
```sh
dever target add linux-aarch64
dever lib install <project-root> --target linux-aarch64
dever build <project-root> --target linux-aarch64 --output <新文件>
```
只用 Dever、没有外部依赖的项目省略 lib install。目标 Lib 必须已有匹配目标的锁;首次生成 ARM 锁使用 lib add/update 的同名 target 参数。准备会显式联网,后续 run/build 离线;缺资源时执行提示的准备命令,不改用系统解释器。target add 准备当前活动版本,锁定其它编译器版本的项目先准备并选择对应版本。
已准备本地发行包时:
```sh
bash install --release /可信发行目录 --trusted-key /独立确认的公钥文件
```
测试或镜像制作增加 `--system-root /已有的自有镜像目录`;不会启动宿主服务。安装器生成的临时 `config/setting.json` 仅供机器 bootstrap 使用,包含本地发行路径、安装根和公钥路径;不是业务配置,不写发行仓库地址或环境变量。
公钥可公开提交;签名私钥绝不能进 skill、源码仓库或发行包。只有已独立确认的公钥才可作为 `--trusted-key`,不能从未知下载包顺手取一把钥匙作为信任依据。
已持有完整 skill 时仍可直接运行 `sudo python3 <skill目录>/scripts/install.py`,它与根 Bash 入口复用同一安装实现。
## 一条命令更新核心与 skill
```sh
sudo dever update
dever version
dever skill path
```
update 只访问内置官方发行源,先准备核心、同版本完整 skill,以及当前活动版本已安装的扩展,再切换活动版本。未使用的扩展不会一起下载;任一准备失败保留原活动版本。普通 check/test/run/build 不检查更新、不隐式联网。
从旧 GitHub 发行源迁移时,需要在新源正式包发布后执行一次新的 `install`。旧启动器和守护进程内置旧地址,普通 update 不负责替换这两个稳定程序;只修改仓库 remote 或重新发布旧包不能完成迁移。新 install 成功后再用 `dever update`。
本仓库直接安装的 skill 在开始开发时先运行 `dever skill path` 并加载该版本的完整 skill,不必每个项目 git pull。也可以用 `dever skill install <新的AI技能目录>/dever-language` 安装稳定的薄入口,之后每次自动解析当前机器版本;已有目录不会被覆盖。不要把新入口嵌套装进已经存在的完整 skill 目录。
项目可选 `config/setting.json` 的 `dever.version` 锁定准确 `major.minor.patch`,未锁定时使用机器活动版本。`dever install 版本` 准备指定版本;`dever use 版本` 显式切换活动版本。update 不改业务项目文件或数据库。锁定项目用 `dever skill path <project-root>` 读取对应指引,不能仅凭活动版指南采用新语法。
## 源码维护者
统一源码仓库为 `ssh://git@git.dever.cc:2222/shemic/dever.git`。`skills/` 是主仓库中的普通目录,与核心、模板和安装器一起提交、校验及发行;无需初始化 submodule 或另行克隆 skill 仓库。普通应用开发者无需克隆核心或编译 Rust。
根 `install` 从 `skills/scripts/install.py` 与 `skills/assets/release.pub` 生成;修改安装器后运行 `python3 sdk/install-entry.py`,交付前运行 `python3 sdk/install-entry.py --check`。禁止单独修改生成物中的安装算法。首个新源发行包发布前,默认下载会明确失败;使用真实签名本地包可验证安装,但不能代表线上安装或更新已经通过。
+83
View File
@@ -0,0 +1,83 @@
# 核心语法
以下片段说明语法。将它们放进正确角色文件并补完整调用链后检查,不能当作免配置 HTTP 项目。
## 类型和函数
```dever
type Greeting {
message: Text
}
greet(name: Text) (response: Greeting) {
response = Greeting { message = "Hello, " + text.trim(name) }
}
```
函数是 `name(输入) (命名输出) { 线性语句 }`,没有 function/return。调用按位置传参。零输入或零输出也写 `()`。单输出直接得到值;多输出调用返回只能按输出名访问的结果。App 的输入、输出和向外传播错误均为公开合同,不暴露 Domain/Adapter 私有类型。
函数命名表达业务行为,调用路径不附加主题文件名。相关函数、类型和同名分句可以同处一个内聚文件;拆分与复用的判断见 [文件与函数组织](development.md#文件与函数组织),不按函数长度自动增加 helper。
常用类型:`Bool`、`Int`(检查溢出)、`Float`、精确 `Decimal`、`Text`、`Bytes`、`Uuid`、`Id`、`DateTime`、`Date`、`Time`、`Duration`、`Secret`、`Json`。可空用 `T?`,集合用 `List<T>`、`Map<K,V>`。Model ID 是每个 Model 独有的名义类型,不等于任意 Int。不得把不同表的 ID 混用。
record 用字段初始化;choice 用分支构造:
```dever
type State {
Draft
Published
Rejected(reason: Text)
}
label(state: State.Published) (text: Text) {
text = "published"
}
label(state: State.Rejected(reason)) (text: Text) {
text = reason
}
label(state: other) (text: Text) {
text = "draft"
}
```
## 分支与边界
没有 if、else、switch、match、条件表达式或递归。同名、同参数数量的分句必须连续,相同输出合同,互斥且穷尽;没有先匹配谁的顺序优先级。`other` 是同一参数位置所有明确模式的补集。
```dever
display(value: Text) (text: Text) {
text = value
}
display(value: null) (text: Text) {
text = "missing"
}
grade(score: Int < 60) (text: Text) {
text = "fail"
}
grade(score: Int >= 60) (text: Text) {
text = "pass"
}
```
支持完整类型、choice 分支、null、Bool/Text/数值字面量、Int/Decimal 范围与 other 模式;多输入检查全部输入组合。任一函数都必须为命名输出赋值。不要写 `return` 提前退出,按分句表示结果与失败。
字面量分句适合明确的状态和规则,不意味着可以按某个用户 ID、租户名字或样例数据写业务特例。运行时配置与可信身份的归属见 [配置与硬编码](development.md#配置与硬编码)。
集合遍历使用语言的 `map/filter/reduce/each` 等核心操作及静态 handler,不能加入 for/while。handler 的签名和可达调用链由编译器检查,不是运行期闭包或动态插件。具体签名查当前版本 `library/` 与核心语言指南,不杜撰 lambda 语法。
## 失败和能力
业务错误通过 choice 的 `error` 分支声明,`fail` 传播,`result(call)` 显式捕获;普通调用直接得到成功值。不能用字符串/任意 JSON 代替类型化失败,不能捕获后悄悄吞掉错误。错误映射到 HTTP 的方式见 application.md。
纯 Domain 函数可声明 `pure`,编译器检查其调用链;要从 Model REST 字段来源调用的业务转换必须是合规的同领域 pure Domain。外部文件/网络 I/O 经 Port/Adapter,不把系统调用塞入普通 App 或 Domain。
Dever 推断挂起,无需 async/await。直接调用按顺序完成;`run` 显式启动结构化子任务,`wait/stop` 消费 Task,任务不能逃逸所属作用域。Group/Channel/Stream 具有静态资源约束;不能跨挂起、线程或请求随意保存句柄。`result` 不改变调度,也不绕开 effect 检查。
`Secret` 不可打印、比较或转为普通 Text。密码使用现有 crypto 能力;不要自制哈希。声明类型、分句和 effect 报错时修正真实边界,不添加无意义中转函数、泛用返回包装或吞错分句来绕过检查。
更多完整语法以官方核心仓库同版本 `LANGUAGE.md` 为准;新建项目先使用本 skill 的已验证模板。
+46
View File
@@ -0,0 +1,46 @@
# 标准能力与依赖
内置 package 不需要 import、下载或把标准库复制到 module/。先查已有能力,避免项目里堆积 parse_id、page_slice、trim 包装函数。
| 能力 | 入口 |
| --- | --- |
| 文本 | `text.trim/lower/upper/contains/starts_with/ends_with/split/replace/join` |
| 数字 | `int.parse/to_text`、`decimal.parse/round/from_int`、`float.parse`、`math` |
| 字节 | `dever.bytes.from_text/to_text/from_ints/length/at/slice/concat` |
| 时间 | `dever.time.now/parse_datetime/format_datetime/parse_date/format_date/duration/add/subtract/difference` |
| 密码与摘要 | `dever.crypto.password_hash/password_verify/token/sha256/hmac_sha256/constant_time_eq` |
| 类型化 JSON | `dever.json`,API/CMD 通常直接复用自动 wire codec |
| HTTP/网络/文件 | `dever.http`、`dever.net`、`dever.io`,业务外部 I/O 按 Port 边界使用 |
| 请求与身份 | `dever.api`、`dever.auth`、`dever.site`,受入口可达性限制 |
这是查找索引,不保证省略的参数/返回值。精确函数签名在官方核心仓库同版本 `library/`,复杂标准能力先读取对应文件和 LANGUAGE.md。`parse` 常返回可空值,不能假定失败得到 0。新版本发布前 skill 索引与库签名须一起验证。
## Dever Package
Package 是可复用 Dever 组件。通过项目 setting.json 中的 `package.registry` 与 `package.use` 声明;显式 `dever package add|list|update|remove|doctor <root> ...` 管理。`dever.lock` 固定版本、摘要和传递依赖;Package 不能覆盖本地同名组件。不要在业务源码中引入 import 或自建 vendor 查找规则。
## 外部 Lib 与普通二进制
第三方生态叫 Lib。项目 `config/setting.json` 的根 lib 数组声明 `pip:name@version`、`npm:name@version` 或 Go 依赖,使用 `dever lib add/list/install/update/remove/doctor` 管理。add/update 自动准备实际需要的运行环境与构建工具;install 按已有锁精确恢复,不改变配置或锁。run/build 只消费锁,不联网,也不读取系统 Python、Node、Go 或 PATH。
锁记录原始来源、摘要、固定构建输入和产物。新机器缺少已构建产物时,install 使用固定输入重建并核对输出摘要;第三方构建不能重现相同字节时会明确失败,不能用更新锁或偷偷重新解析版本冒充恢复。检查详细错误后,由用户决定是否执行 update 重新生成锁。
Lib 必须通过领域 Port/Adapter 使用。Port 声明类型与失败合同,Adapter 实现它;App 只调用 Port。声明的类型、返回值、业务失败、超时和 capability 使用统一 Worker 协议,各语言 SDK 不替代编译器类型检查。Python/JS/Go SDK 与可用 runtime 必须匹配 Dever 发行版本。
Dever 不检查第三方语言源码内部的函数拆分、重复实现和业务硬编码;Port 类型与 Worker 协议验证不能代替这部分审查。外部生态承接确需复用的现成能力,不能用于转移普通业务逻辑以规避 Dever 约束,开发规则仍见 [项目开发](development.md)。
普通 Linux 二进制不需要 Worker 协议。领域 `port.dever`:
```dever
execute(args: List<Text>, stdin: Bytes?) (output: dever.process.Output) fails dever.process.Error
```
`adapter.dever`:
```dever
external command "bin/tool" {}
```
将相应架构 ELF 放在领域的 bin/tool,App 调用 `port.execute(["--version"], null)`。结果有 code、stdout、stderr,字节显式转文本;参数不是 shell 字符串。非零退出码仍是结果,启动失败/超时/超限是错误。文件、网络、子进程能力显式声明且受配置授权,不能关闭沙箱来掩盖部署问题。
测试必须使用 Port fake,不执行真实第三方程序。跨到 ARM 时必须提供目标架构的库和二进制,Dever 不转换已有 x86 ELF。
+15
View File
@@ -0,0 +1,15 @@
# Markdown 源码
使用 `dever new <new-directory> --markdown` 获得经过验证的模板;不要只把任意 Markdown 中的代码围栏改名当作完整程序。
文件后缀 `.dever.md`;编译器只编译顶层 `dever` 或 `typescript dever` 围栏,且校验说明与代码合同。普通 `.md` 不是源码,两种源码格式的类型、目录、访问、权限约束一致。
- 每个文件一个一级标题,随后写用途、包、公开类型、公开方法、使用;空项写“无”。包说明来自路径,不是源码 package 声明。
- 每个逻辑声明有自己的二级标题、用途和唯一代码块,一级标题下不直接放源码。
- 类型写 `- 类型:` 与字段/分支说明;函数写 `- 函数:`、输入、输出,名称、类型、顺序与代码一致。每个字段说明包含非空中文用途。
- 同名同参数数量的所有分句放同一章节、同一围栏;不同签名或类型分开。
- API 写 `- 声明:cmd greet`、`post publish`、`rest model` 等,与代码一致,不虚构公开方法。
- App 自动公开其类型/函数;Domain、Adapter、API 和测试没有公开方法。调用清单隐藏 app role 和 app topic。
- 测试同样需要完整说明,唯一入口仍是文件同名零输入零输出函数。
维护源码与说明后一起跑 fmt --check、check。完整声明说明协议见官方核心仓库同版本 MARKDOWN-SYNTAX.md。
+266
View File
@@ -0,0 +1,266 @@
#!/usr/bin/env python3
"""Install an authenticated Dever release; no application configuration is read."""
import argparse
import hashlib
import json
import os
from pathlib import Path, PurePosixPath
import re
import ssl
import subprocess
import sys
import tempfile
import urllib.error
import urllib.parse
import urllib.request
RELEASES = "https://git.dever.cc/shemic/dever/dist"
MAX_METADATA = 2 * 1024 * 1024
MAX_RELEASE = 2 * 1024 * 1024 * 1024
ASSET_HOSTS = {"git.dever.cc"}
class OfficialRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, request, stream, code, message, headers, address):
target = urllib.parse.urlsplit(address)
if (target.scheme != "https" or target.hostname not in ASSET_HOSTS
or target.username or target.password or target.port not in (None, 443)
or target.fragment):
raise ValueError("发行下载重定向离开官方 HTTPS 站点")
return super().redirect_request(request, stream, code, message, headers, address)
def download(address):
# Installation does not inherit HTTP proxy/environment configuration.
client = urllib.request.build_opener(
urllib.request.ProxyHandler({}), OfficialRedirect(),
urllib.request.HTTPSHandler(context=ssl.create_default_context()),
)
try:
return client.open(address, timeout=60)
except urllib.error.HTTPError as error:
if error.code == 404:
raise ValueError("该版本/平台尚无官方发行包,未修改已安装的 Dever") from error
raise ValueError(f"官方发行下载失败:HTTP {error.code}") from error
def platform_name():
host = os.uname()
names = {"x86_64": "linux-x86_64", "aarch64": "linux-aarch64"}
if host.sysname != "Linux" or host.machine not in names:
raise ValueError("安装器当前只支持 Linux x86_64/aarch64,且需对应平台已发布工具包")
return names[host.machine]
def prerequisites(platform):
if sys.version_info < (3, 9):
raise ValueError("安装需要 /usr/bin/python3 3.9 或更新版本")
if not os.access("/usr/bin/openssl", os.X_OK):
raise ValueError("安装需要 /usr/bin/openssl(支持 Ed25519)")
try:
libc = os.confstr("CS_GNU_LIBC_VERSION") or ""
except (OSError, ValueError):
libc = ""
match = re.fullmatch(r"glibc ([0-9]+)\.([0-9]+)", libc)
if not match:
raise ValueError("官方 Linux 工具包需要 glibc,当前系统 ABI 不支持")
if platform == "linux-x86_64" and tuple(map(int, match.groups())) < (2, 39):
raise ValueError("当前 Linux x86_64 工具包需要 glibc 2.39 或更新版本")
def read_bounded(path, limit):
with path.open("rb") as stream:
contents = stream.read(limit + 1)
if len(contents) > limit:
raise ValueError("发行元数据超过大小限制")
return contents
def metadata(release, source, selection, platform):
for filename in ("manifest.json", "manifest.sig"):
if source:
contents = read_bounded(source / filename, MAX_METADATA)
else:
with download(f"{RELEASES}/{selection}/dever-{platform}.{filename}") as stream:
contents = stream.read(MAX_METADATA + 1)
if len(contents) > MAX_METADATA:
raise ValueError("发行元数据超过大小限制")
(release / filename).write_bytes(contents)
def authenticate(release, key_path, scratch, platform, requested):
key = bytes.fromhex(read_bounded(key_path, 256).decode().strip())
signature = bytes.fromhex(read_bounded(release / "manifest.sig", 256).decode().strip())
if len(key) != 32 or len(signature) != 64:
raise ValueError("发行公钥或签名长度错误")
public_der = scratch / "public.der"
public_der.write_bytes(bytes.fromhex("302a300506032b6570032100") + key)
signature_file = scratch / "signature.bin"
signature_file.write_bytes(signature)
verified = subprocess.run(
["/usr/bin/openssl", "pkeyutl", "-verify", "-pubin", "-keyform", "DER",
"-inkey", str(public_der), "-rawin", "-in", str(release / "manifest.json"),
"-sigfile", str(signature_file)],
env={}, capture_output=True, timeout=15, check=False,
)
if verified.returncode:
raise ValueError("发行签名校验失败,未执行安装程序")
manifest = json.loads((release / "manifest.json").read_text())
if (set(manifest) != {"format", "version", "platform", "artifacts", "extensions"}
or manifest.get("format") != "dever-release-v2" or manifest.get("platform") != platform
or not isinstance(manifest["extensions"], list) or len(manifest["extensions"]) > 12):
raise ValueError("发行格式或平台不匹配")
version = manifest.get("version", "")
if not re.fullmatch(r"(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)", version):
raise ValueError("发行版本不是准确的 major.minor.patch")
if requested != "latest" and version != requested:
raise ValueError("发行版本与请求不匹配")
artifacts = manifest.get("artifacts", [])
if not 1 <= len(artifacts) <= 4096:
raise ValueError("发行文件数超限")
catalog = {}
total = 0
for artifact in artifacts:
name, size, digest = artifact["path"], artifact["bytes"], artifact["sha256"]
if (set(artifact) != {"path", "bytes", "sha256"}
or not name or name.startswith("/")
or any(part in ("", ".", "..") for part in name.split("/"))
or any(character in name for character in ("\\", ":", "\0"))
or str(PurePosixPath(name)) != name
or name in catalog or name in ("manifest.json", "manifest.sig")):
raise ValueError("发行包含不安全或重复路径")
if type(size) is not int or size < 0 or not re.fullmatch(r"[0-9a-f]{64}", digest):
raise ValueError("发行文件长度或摘要错误")
total += size
if total > MAX_RELEASE:
raise ValueError("发行解压长度超过 2 GiB")
catalog[name] = artifact
if not {"dever-core", "bootstrap/dever", "skills/dever-language/SKILL.md"} <= catalog.keys():
raise ValueError("首装包缺少核心、bootstrap 启动器或配套 skill")
return version, catalog, key
def copy_artifact(stream, destination, artifact):
destination.parent.mkdir(parents=True, exist_ok=True)
remaining = artifact["bytes"]
digest = hashlib.sha256()
with destination.open("xb") as output:
while remaining:
contents = stream.read(min(remaining, 1024 * 1024))
if not contents:
raise ValueError("发行文件被截断")
output.write(contents)
digest.update(contents)
remaining -= len(contents)
if digest.hexdigest() != artifact["sha256"]:
raise ValueError("发行文件摘要校验失败")
executable = artifact["path"] in {"dever-core", "bootstrap/dever", "bootstrap/deverd"}
destination.chmod(0o755 if executable else 0o644)
def payload(release, source, version, platform, catalog, trusted_key=None):
if source:
for name, artifact in catalog.items():
path = source / name
for ancestor in (path, *path.parents):
if ancestor.is_symlink():
raise ValueError("本地发行文件不能经过符号链接")
if ancestor == source:
break
if not path.is_file() or path.stat().st_size != artifact["bytes"]:
raise ValueError("本地发行文件类型或长度错误")
with path.open("rb") as stream:
copy_artifact(stream, release / name, artifact)
return
if trusted_key is None:
raise ValueError("网络发行解压需要独立信任公钥")
# Only signed bytes are executable. Keep the helper separate so Rust can
# create_new every archive member and enforce one complete codec/TAR contract.
with tempfile.TemporaryDirectory(prefix=".extract-", dir=release.parent) as directory:
scratch = Path(directory)
helper = scratch / "helper"
base = f"{RELEASES}/{version}/dever-{platform}"
for name, artifact in catalog.items():
if name == "bootstrap/dever" or name.startswith("bootstrap/lib/"):
with download(f"{base}.blob-{artifact['sha256']}") as stream:
copy_artifact(stream, helper / name, artifact)
if stream.read(1):
raise ValueError("bootstrap 下载超过签名长度")
compressed = scratch / "base.tar.zst"
download_archive(f"{base}.tar.zst", compressed)
configuration = scratch / "extract.json"
configuration.write_text(json.dumps({
"release": str(release), "archive": str(compressed),
"trusted_key": str(trusted_key),
}))
subprocess.run([str(helper / "bootstrap/dever"), "--dever-extract", str(configuration)],
env={}, check=True, timeout=180)
def download_archive(address, destination):
remaining = MAX_RELEASE
with download(address) as stream, destination.open("xb") as output:
while contents := stream.read(min(remaining + 1, 1024 * 1024)):
remaining -= len(contents)
if remaining < 0:
raise ValueError("发行下载超过 2 GiB")
output.write(contents)
def install(arguments, default_key=None):
if os.geteuid() != 0:
raise ValueError("机器安装需要管理员权限;先检查脚本,再用 sudo python3 执行")
platform = platform_name()
prerequisites(platform)
if arguments.version != "latest" and not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", arguments.version):
raise ValueError("--version 需要 latest 或准确版本")
system_root = arguments.system_root.absolute()
if not system_root.is_dir() or system_root.is_symlink():
raise ValueError("--system-root 必须是已存在的真实目录")
source = arguments.release.absolute() if arguments.release else None
if source and (not source.is_dir() or source.is_symlink()):
raise ValueError("--release 必须是已存在的真实发行目录")
# The root-owned staging also keeps the pinned key outside the downloaded release.
with tempfile.TemporaryDirectory(prefix=".dever-install-", dir=system_root) as directory:
scratch = Path(directory)
release = scratch / "release"
release.mkdir()
metadata(release, source, arguments.version, platform)
key_path = arguments.trusted_key
if key_path is None:
key_path = scratch / "independent-release.pub"
key_path.write_text(default_key)
version, catalog, key = authenticate(release, key_path, scratch, platform, arguments.version)
trusted = scratch / "release.pub"
trusted.write_text(key.hex() + "\n")
payload(release, source, version, platform, catalog, trusted)
configuration = scratch / "config"
configuration.mkdir()
(configuration / "setting.json").write_text(json.dumps({
"release": str(release), "system_root": str(system_root),
"trusted_key": str(trusted),
"activate_service": system_root == Path("/") and not arguments.no_start,
}))
subprocess.run([str(release / "bootstrap/dever"), "--dever-install", str(scratch)],
env={}, check=True, timeout=180)
print(f"Dever {version} 已安装到 {system_root / 'opt/dever'}")
def main(default_key=None):
parser = argparse.ArgumentParser(prog="dever install", description=__doc__)
parser.add_argument("--version", default="latest")
parser.add_argument("--release", type=Path, help="使用本地已签名发行目录,不下载")
key_path = None if default_key is not None else Path(__file__).resolve().parents[1] / "assets/release.pub"
parser.add_argument("--trusted-key", type=Path, default=key_path)
parser.add_argument("--system-root", type=Path, default=Path("/"), help="已有的自有安装镜像目录;默认安装当前机器")
parser.add_argument("--no-start", action="store_true", help="不启动当前机器 systemd 服务")
arguments = parser.parse_args()
try:
install(arguments, default_key)
except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as error:
parser.exit(1, f"Dever 安装失败:{error}\n")
if __name__ == "__main__":
main()