本文档描述两种 DSL 的完整语法:
.aosproj— 声明一个包的内容、元信息与构建矩阵,由apkgCLI 消费manifest.xml(v2)— 嵌入.apkg归档内的分发清单,由 Apkg 服务器消费
.aosproj 是 MSBuild 风格的 XML 文件,描述如何把本地文件打成 .deb。
<Project Sdk="Aiursoft.Apkg.Sdk">
<PropertyGroup>
<!--
PackageName → deb 的包名,同时也是将来 apt install <name> 的名字。
只允许小写字母、数字、连字符(-)、加号(+)和点(.),
首字符必须为字母或数字,不允许下划线或大写。
正则:^[a-z0-9][a-z0-9\-+.]*$
-->
<PackageName>anduinos-logo</PackageName>
<!--
PackageVersion → 遵循 Debian 版本规范。
纯数字点分(1.0.0)是最常见形式。
-->
<PackageVersion>1.0.0</PackageVersion>
<!--
PackageAuthors → 填入 deb 的 Maintainer 字段。
格式:姓名 <email>
-->
<PackageAuthors>AnduinOS Team <dev@anduinos.com></PackageAuthors>
<!-- deb 的 Description 字段,一行简介 -->
<PackageDescription>AnduinOS logo assets</PackageDescription>
<!--
TargetDistro → 目标发行版的字符串标识,自由命名。
必须与 Apkg 服务器上配置的仓库 Distro 字段一致。
例:anduinos、ubuntu、debian。
-->
<TargetDistro>anduinos</TargetDistro>
<!--
TargetSuites → 空格分隔的 suite 列表。
每个 suite 单独产出一个 .deb 文件。
例:"resolute questing" 会产出两个 .deb。
-->
<TargetSuites>resolute</TargetSuites>
<!--
TargetArchitectures → 空格分隔的架构列表,或 "all" 表示架构无关。
"all" 适合纯脚本、字体、图标等不含二进制的包。
多架构示例:"amd64 arm64"
-->
<TargetArchitectures>all</TargetArchitectures>
<!--
Component → APT 仓库中的组件分类,通常填 main。
其余合法值取决于服务器上仓库的 Components 配置。
-->
<Component>main</Component>
</PropertyGroup>
<ItemGroup>
<!--
IncludeFile → 把一个文件安装到系统指定路径。
Include = 相对于 .aosproj 所在目录的源文件路径。
Target = 安装后在系统里的绝对路径(含文件名)。
-->
<IncludeFile Include="logo.svg" Target="/usr/share/pixmaps/anduinos-logo.svg" />
</ItemGroup>
</Project>| 字段 | 必填 | 说明 |
|---|---|---|
PackageName |
✅ | deb 包名,正则 ^[a-z0-9][a-z0-9\-+.]*$(小写字母、数字、-、+、.,首字符须为字母或数字) |
PackageVersion |
✅ | 版本号,遵循 Debian 版本规范(如 1.0.0)。支持 $(UpstreamVersion) 变量自动从上游 .deb 的 Version 字段派生,如 $(UpstreamVersion)-anduinos |
PackageDescription |
✅ | 包的单行简介 |
TargetSuites |
✅ | 空格分隔的 suite 列表(如 resolute questing),每个 suite 产出一个 .deb |
PackageAuthors |
默认 Maintainer,可被 Maintainer 字段覆盖。lint Warning 但 Maintainer 可替代 |
|
TargetDistro |
发行版标识(如 anduinos、ubuntu)。lint Warning,构建全部 target 时硬依赖,单次 build 默认 "ubuntu" |
|
TargetArchitectures |
空格分隔的架构列表(如 amd64 arm64),或 all 表示架构无关。lint Warning,构建全部 target 时硬依赖 |
|
Component |
— | APT 组件,默认为 main |
PackageHomepage |
— | 项目主页 URL |
RepositoryUrl |
— | 源码仓库 URL(如 GitHub 链接)。传递至 manifest 和 ApkgPackage,供 Web UI 展示。不进 DEBIAN/control |
LicenseType |
— | SPDX 标识符,如 MIT、GPL-2.0 |
LicenseFile |
— | 许可证文件相对路径 |
PackageTags |
— | 逗号分隔的包标签。目前保留为未来使用,暂无后端路由支持 |
Maintainer |
— | 覆盖 PackageAuthors 作为 deb 的 Maintainer 字段 |
Provides |
— | deb Provides 字段,声明此包提供哪些虚包 |
Conflicts |
— | deb Conflicts 字段,声明与哪些包冲突 |
Replaces |
— | deb Replaces 字段,声明此包替换哪些旧包 |
Breaks |
— | deb Breaks 字段,声明此包会破坏哪些其他包的特定版本。本地优先,未填时回退到上游值。仅在需要阻断旧版本兼容时才需填写 |
Recommends |
— | deb Recommends 字段,声明强烈推荐但非必须的软件包(apt install 默认安装,apt remove 时不会破坏依赖) |
Suggests |
— | deb Suggests 字段,声明可选的锦上添花软件包(apt 不自动安装,仅作提示) |
Section |
— | deb Section 字段(如 utils, admin, editors)。三级回退:本地 → 上游 → Debian 标准默认 "utils"。影响 APT 分类搜索 |
Priority |
— | deb Priority 字段(如 optional, required, important)。三级回退:本地 → 上游 → Debian 标准默认 "optional"。通常保持默认即可 |
DependencyCheckSource |
— | ItemGroup 元素,声明一个用于验证依赖是否存在的 APT 仓库源。支持配置多个源,任一源命中即通过(union 语义)。属性:Url(必填,APT 服务器 base URL)、SuiteMap(可选,目标 suite → 检查 suite 映射)、Condition(可选,MSBuild 风格条件过滤)。不配置任何源则跳过依赖检查。详见 §依赖验证 |
SuiteShortNameMap |
— | 将目标 suite 名映射为短名,供 $(SuiteShortName) 版本占位符使用。格式与 UpstreamSuiteMapping 相同:空格/逗号分隔的 target=short 对(如 noble-addon=noble questing-addon=questing)。若留空则 $(SuiteShortName) 回退为完整 suite 名 |
UpstreamUrl |
上游 APT 仓库的 base URL(如 http://archive.ubuntu.com/ubuntu)。设置 UpstreamPackage 时必填。支持多条声明,通过 Condition 属性区分架构,实现多架构上游源路由(如 amd64 → archive.ubuntu.com,arm64 → ports.ubuntu.com)。也支持 $(Arch) 等变量解析 |
|
UpstreamDistro |
上游仓库的发行版标识(如 ubuntu)。设置 UpstreamPackage 时必填 |
|
UpstreamPackage |
— | 上游 .deb 的包名(如 base-files)。一旦设置,触发上游派生模式 |
UpstreamSuite |
上游 suite(如 $(Suite) 表示与构建 suite 同名)。设置 UpstreamPackage 时必填 |
|
UpstreamSuiteMapping |
— | 输出 suite → 上游 suite 的映射表。格式:out1=up1, out2=up2。当 UpstreamSuite 解析后的值命中了映射的 key,则替换为对应的上游 suite |
UpstreamComponent |
— | 上游 APT 组件,默认为 main。支持 $(Suite) 等变量 |
UpstreamArch |
— | 上游包架构,默认为 all。支持 $(Arch) 等变量,构建时自动替换为当前目标架构(如 amd64、arm64),实现多架构上游派生 |
UpstreamSignedBy |
— | 上游 APT 仓库的 GPG 公钥文件路径(相对于 .aosproj 所在目录)。设置后,apt-get update 会通过 [signed-by=…] 选项用该公钥验证上游仓库签名;未设置时 file:// URL 自动使用 [trusted=yes],其他 URL 使用系统信任库。详见 §上游派生-GPG 签名验证 |
SuppressUpstreamScripts |
— | 设为 true 时,不继承上游包的 maintainer scripts(preinst/postinst/prerm/postrm),仅继承其数据载荷。适用于仅需上游文件但需完全自控安装脚本的场景。默认为 false |
SuppressUpstreamDependencies |
— | 空格/逗号分隔的上游包名列表,在合并本地依赖前从上游继承的 Depends 中移除。例如 "ubuntu-pro-client ubuntu-advantage-desktop-daemon"。移除时仅匹配基础包名(忽略版本约束),大小写不敏感 |
当 .aosproj 设置了 <UpstreamPackage> 时,apkg build 进入上游派生模式——不从零构建,而是从一个已存在的 .deb 派生:
- 下载:通过隔离的
apt-get从UpstreamUrl的UpstreamSuite下载UpstreamPackage - 解包:用
dpkg-deb -x提取上游数据到暂存区,用dpkg-deb -e提取控制文件 - PrebuildCommand:执行预构建命令(此时可对上游文件执行
sed等操作) - 合并:本地条目(
IncludeFile、IncludeScript、IncludeFolder)覆盖到暂存区 - 合并 control 字段:
- Version:若
PackageVersion包含$(UpstreamVersion),则替换为上游的实际版本号(如13.1ubuntu1)。这使得派生包的版本自动跟随上游 Depends:上游依赖在前,本地Dependency附录在后。以基础包名去重(如上游有libc6 (>= 2.34)而本地声明libc6,保留上游版本)。设置SuppressUpstreamDependencies可在合并前按包名移除上游依赖Provides、Conflicts、Replaces、Breaks、Recommends、Suggests:本地优先,未填时回退到上游值Homepage:本地优先,未填时回退到上游值Section、Priority:三级回退 — 本地优先,未填时回退到上游值,上游也没有时使用 Debian 标准默认值("utils"/"optional")
- Version:若
- 链式 maintainer scripts:上游
preinst/postinst/prerm/postrm(去除 shebang)→ 本地脚本 → systemd 自动脚本,按序追加。设置<SuppressUpstreamScripts>true</SuppressUpstreamScripts>可跳过上游脚本,仅保留本地脚本和 systemd 自动脚本
这是 AnduinOS 替换 Ubuntu base-files(Essential: yes)等基础包的推荐模式:通过 APT pinning 设置 Pin-Priority: 1001,让 AnduinOS 的派生包覆盖 Ubuntu 原包,同时保持一切兼容。
base-files 示例:
<Project Sdk="Aiursoft.Apkg.Sdk">
<PropertyGroup>
<PackageName>base-files</PackageName>
<PackageVersion>$(UpstreamVersion)-anduinos</PackageVersion>
<PackageDescription>AnduinOS base files (derived from Ubuntu)</PackageDescription>
<Maintainer>AnduinOS Team <dev@anduinos.com></Maintainer>
<TargetDistro>anduinos</TargetDistro>
<TargetSuites>resolute questing</TargetSuites>
<TargetArchitectures>amd64 arm64</TargetArchitectures>
<!-- 上游派生:从 Ubuntu 下载 base-files .deb 并在此基础上叠加本地文件 -->
<!-- 多架构上游源路由:amd64 → archive,arm64 → ports -->
<UpstreamUrl Condition="'$(Arch)' == 'amd64'">http://archive.ubuntu.com/ubuntu</UpstreamUrl>
<UpstreamUrl Condition="'$(Arch)' == 'arm64'">http://ports.ubuntu.com</UpstreamUrl>
<UpstreamDistro>ubuntu</UpstreamDistro>
<UpstreamPackage>base-files</UpstreamPackage>
<UpstreamSuite>$(Suite)</UpstreamSuite>
<UpstreamComponent>main</UpstreamComponent>
<UpstreamArch>$(Arch)</UpstreamArch>
</PropertyGroup>
<!-- 覆盖上游文件:用 AnduinOS 定制的 /etc/issue, /etc/os-release 等替换上游文件 -->
<ItemGroup>
<IncludeFile Include="deploy/issue" Target="/etc/issue" />
<IncludeFile Include="deploy/os-release" Target="/etc/os-release" />
<IncludeFile Include="deploy/lsb-release" Target="/etc/lsb-release" />
</ItemGroup>
</Project>$(Suite) 变量会在构建时解析为 resolute、questing 等,实现同一 .aosproj 从不同上游 suite 下载对应版本。$(Arch) 同理——在 UpstreamArch 中使用 $(Arch) 时,构建 amd64 target 会从上游下载 amd64 包,构建 arm64 target 会下载 arm64 包,实现单一 .aosproj 的多架构上游派生。
多架构上游源路由:Ubuntu 的 amd64 包在 archive.ubuntu.com,arm64 包在 ports.ubuntu.com——两个域互不包含对方架构。通过声明多条带 Condition 的 <UpstreamUrl>,构建时根据当前目标架构自动选择正确的上游源。Condition 采用 first-match 语义:找到第一个满足条件的即停止,未声明 Condition 的条目作为默认 fallback。
上游下载使用隔离的 apt-get update + apt-get download。当构建主机通过 dpkg --add-architecture 注册了外部架构(例如 amd64 主机注册了 arm64 以便交叉编译),apt 默认会为所有已注册架构拉取 Package 索引。对于只托管单一架构的上游镜像(如 archive.ubuntu.com 没有 arm64),这会导致 binary-arm64/Packages 返回 404,构建失败。
Apkg 自动在临时 apt source 上附加 [arch=<构建目标架构>] 限定符,阻止 apt 拉取外部架构索引。此行为对 .aosproj 开发者完全透明——无需任何配置。
设计要点(v10.0.31+):
- 限定符始终使用构建目标架构(即
TargetArchitectures中当前正在构建的 arch),而非UpstreamArch。即便上游包为all架构,仍会附加[arch=amd64]或[arch=arm64] binary-all包始终被合并到每个架构的 Package 索引中(Debian 标准约定),因此用构建目标架构限定不会阻止下载Architecture: all的上游包- 此机制与
UpstreamArch变量解析完全解耦:UpstreamArch决定下载哪个 .deb,[arch=...]只决定拉取哪些索引
$(UpstreamVersion) 变量仅在 <PackageVersion> 中可用。构建时,Apkg 从下载的上游 .deb 控制文件中读取 Version 字段并替换该占位符。例如 <PackageVersion>$(UpstreamVersion)-anduinos</PackageVersion> 对 noble suite(上游版本为 13ubuntu10)会生成 13ubuntu10-anduinos,对 questing suite(上游版本为 14ubuntu3)会生成 14ubuntu3-anduinos。
当输出 suite 名与上游 suite 名不同时,可通过 <UpstreamSuiteMapping> 建立映射。典型场景:AnduinOS 的 addon 仓库使用 questing-addon 等独立 suite(避免与 Official 仓库的 questing 冲突),但上游包仍需从 Ubuntu 的 questing 下载:
<PropertyGroup>
<TargetSuites>noble-addon questing-addon resolute-addon</TargetSuites>
<UpstreamSuite>$(Suite)</UpstreamSuite>
<UpstreamSuiteMapping>noble-addon=noble, questing-addon=questing, resolute-addon=resolute</UpstreamSuiteMapping>
</PropertyGroup>构建时,$(Suite) 先展开为 noble-addon,然后通过映射表查找到上游 suite noble,最终从 Ubuntu 的 noble 下载原包。不设映射时行为不变——解析后的 suite 直接用于上游下载。
从 HTTPS 上游 APT 仓库下载包时,APT 默认依赖系统的信任库验证仓库签名。在某些环境中(如容器构建、未导入上游公钥的系统),签名验证会失败。Apkg 通过 UpstreamSignedBy 提供显式的公钥管理:
<PropertyGroup>
<!-- 指向项目仓库中的 GPG 公钥文件,相对于 .aosproj 所在目录 -->
<UpstreamSignedBy>keys/anduinos-archive-keyring.gpg</UpstreamSignedBy>
</PropertyGroup>优先级(从高到低):
| 配置 | apt 选项 | 说明 |
|---|---|---|
UpstreamSignedBy 已设置 |
[signed-by=/path/to/keyring.gpg] |
用项目指定的公钥验证上游签名。公钥文件在构建时自动复制到隔离的 apt 临时目录,构建结束后自动清理 |
UpstreamSignedBy 未设置,UpstreamUrl 为 file:// |
[trusted=yes] |
本地文件仓库,跳过签名验证 |
UpstreamSignedBy 未设置,UpstreamUrl 为远程 URL |
无选项 | 使用系统信任库(如 /etc/apt/trusted.gpg.d/)验证 |
文件要求:
- 公钥文件必须是有效的 GPG keyring 格式(可用
gpg --export --armor导出) - 文件必须提交到项目仓库中(
.aosproj同级或子目录) - 构建时会检查文件是否存在,不存在则抛出
InvalidOperationException - 文件名会被保留复制到临时目录,不会修改
与 PrebuildCommand 的协作:如果公钥文件由 PrebuildCommand 动态生成,lint 阶段仅发出 Warning(不阻止构建)。构建时会做硬错误检查——文件缺失则中止。
apkg lint 会联网验证所有 <Dependency> 和 <Recommend> 声明中的包名是否在配置的 APT 源中真实存在。通过 <DependencyCheckSource> ItemGroup 元素配置一个或多个验证源,union 语义——包只要在任意一个源中存在就通过验证。
<ItemGroup>
<!-- 源 1: Ubuntu 官方镜像 — 验证标准 Ubuntu 包 -->
<DependencyCheckSource
Url="https://archive.ubuntu.com/ubuntu"
SuiteMap="noble-addon=noble questing-addon=questing resolute-addon=resolute" />
<!-- 源 2: AnduinOS 私有仓库 — 验证 AnduinOS 定制包 -->
<DependencyCheckSource
Url="https://apkg-dev.aiursoft.com/artifacts/anduinos"
SuiteMap="noble-addon=noble-addon questing-addon=questing-addon resolute-addon=resolute-addon" />
</ItemGroup>属性说明:
| 属性 | 必填 | 说明 |
|---|---|---|
Url |
是 | APT 仓库 base URL(如 https://archive.ubuntu.com/ubuntu) |
SuiteMap |
否 | 目标 suite → 检查 suite 映射,格式与 UpstreamSuiteMapping 相同。不设时直接使用目标 suite 名查询 |
Condition |
否 | MSBuild 风格条件。仅当条件为真时才使用该源(如 '$(Suite)' != 'jammy') |
行为:
- 不配置任何
DependencyCheckSource则跳过依赖验证(opt-out) - 任一条目中的任一备选包名(
|分隔)在任一源中存在即认为通过 - 版本约束(如
gnome-shell (>= 46~))会被自动剥离,仅验证包名 - 网络错误 per-source 产生 Warning,不会阻止验证继续或构建中止
Provides字段也会被检查(通过 APT Packages.gz 中的 Provides 行)
apkg lint 可单独执行,也由 apkg build 在构建前自动调用。Error 级别问题会中止构建,Warning 仅打印提示。以下是它检查的全部规则:
| 规则 | 级别 |
|---|---|
PackageName、PackageVersion、PackageDescription、TargetSuites 必须设置 |
Error |
PackageName 必须匹配 ^[a-z0-9][a-z0-9\-+.]*$ |
Error |
所有条目的 Target= 不能为空 |
Error |
所有条目的 Include=(Source)不能为空 |
Error |
Condition 表达式必须能被解析 |
Error |
Maintainer 与 PackageAuthors 至少填一个 |
Warning |
UpstreamPackage 设置时必须同时设置 UpstreamUrl、UpstreamDistro、UpstreamSuite |
Error |
UpstreamPackage 设置时 UpstreamComponent 未填(默认 main) |
Warning |
UpstreamPackage 设置时 UpstreamArch 未填(默认 all) |
Warning |
UpstreamSuiteMapping 中输出 suite 未在 TargetSuites 中声明 |
Warning |
UpstreamSuiteMapping 中上游 suite 为空 |
Error |
TargetSuites 中某 suite 未出现在 UpstreamSuiteMapping 中 |
Warning |
TargetDistro 未设(构建全部 target 时必须,默认回退 ubuntu) |
Warning |
TargetArchitectures 未设(构建全部 target 时必须) |
Warning |
UpstreamSignedBy 指向的公钥文件不存在(可能由 PrebuildCommand 生成,仅提示) |
Warning |
<DependencyCheckSource> Url 为空 |
Error |
<DependencyCheckSource> 未设 SuiteMap(suite 名将原样使用) |
Warning |
DependencyCheckSource Condition 语法无效 |
Error |
所有 Include= 指向的源文件/目录在磁盘上实际存在 |
Warning |
至少声明一个文件条目(IncludeFile/IncludeScript/IncludeFolder/ConfFile),否则包为空 |
Warning |
所有条目都支持 MSBuild 风格的 Condition 属性,支持以下运算符和变量。
变量:$(Distro)、$(Suite)、$(Arch)(别名 $(Architecture))、$(Component)、$(UpstreamDistro)、$(UpstreamSuite)、$(UpstreamArch)(别名 $(UpstreamArchitecture))。此外 $(UpstreamVersion) 可在 PackageVersion 中用作模板变量,构建时自动替换为上游版本号。
运算符:
| 运算符 | 说明 | 示例 |
|---|---|---|
== |
等于 | '$(Arch)' == 'amd64' |
!= |
不等于 | '$(Suite)' != 'noble-addon' |
and |
逻辑与 | '$(Suite)' == 'questing-addon' and '$(Arch)' == 'amd64' |
or |
逻辑或 | '$(Suite)' == 'noble-addon' or '$(Suite)' == 'questing-addon' |
and 优先级高于 or,字符串比较大小写不敏感。未指定 Condition 属性(或为空)表示该条目始终生效。
<!--
把一个文件原样安装到系统路径。
适合图片、字体、数据文件、库文件等。
Include = 相对于 .aosproj 的源路径
Target = 安装后的完整绝对路径,含文件名
-->
<IncludeFile Include="src/logo.svg" Target="/usr/share/pixmaps/anduinos-logo.svg" />
<!-- 带条件:只在 amd64 上包含特定库 -->
<IncludeFile
Include="lib/amd64/libfoo.so"
Target="/usr/lib/x86_64-linux-gnu/libfoo.so"
Condition="'$(Arch)' == 'amd64'" />Mode 属性(可选):
IncludeFile、IncludeScript、ConfFile 支持可选的 Mode 属性,用于设置文件的 Unix 权限。
值为 3 位八进制字符串,如 "755"、"644"、"600"。
- 对
IncludeFile、ConfFile和SystemdUnit:不设Mode时默认为644(不可执行) - 对
IncludeScript:不设Mode时默认为755(可执行)
<!-- 二进制文件设为 755(可执行) -->
<IncludeFile Include="build/my-binary" Target="/usr/bin/my-binary" Mode="755" />
<!-- 配置文件设为 644(仅 owner 可写) -->
<IncludeFile Include="config/defaults.json" Target="/etc/myapp/defaults.json" Mode="644" />
<!-- 不设 Mode,保留源文件原始权限 -->
<IncludeFile Include="data/logo.svg" Target="/usr/share/pixmaps/logo.svg" /><!--
与 IncludeFile 相同,但安装后自动将目标文件权限设为 0755(可执行)。
可通过 Mode 属性覆盖默认权限:
<IncludeScript Include="bin/readonly-script" Target="/usr/bin/readonly-script" Mode="644" />
不设 Mode 时默认 755。
适合放在 /usr/bin/、/usr/sbin/、/usr/lib/xxx/bin/ 下的脚本或二进制。
-->
<IncludeScript Include="bin/my-tool" Target="/usr/bin/my-tool" /><!--
递归安装整个目录。目录下的所有文件会保持相对结构
安装到 Target 指定的路径下。
Include = 相对于 .aosproj 的源目录路径
Target = 安装后的目录路径
符号链接行为(v10.0.4 起):
- 文件符号链接(如 libfoo.so -> libfoo.so.1)会被原样保留为符号链接,
不会展开(dereference)为实体文件拷贝。
- 目录符号链接(如 codecs/alias -> codecs/real)会被原样保留为目录
符号链接,不会递归进入。这避免了同一文件以多条路径重复出现在
最终 .deb 包中,也杜绝了循环符号链接导致的无限枚举。
- 普通文件和目录的行为不变:正常递归复制。
-->
<IncludeFolder Include="assets/" Target="/usr/share/anduinos/assets" /><!--
安装一个配置文件,并将其路径写入 deb 的 conffiles 列表。
效果:升级包时,若用户已修改该文件,dpkg 会提示是否用新版覆盖,
而不是静默覆盖——这是 Debian 对用户配置的标准保护机制。
适合放在 /etc/ 下的用户可编辑配置。
-->
<ConfFile Include="config/settings.conf" Target="/etc/anduinos/settings.conf" /><!--
每个 Dependency 对应 deb control 文件 Depends 字段中的一项。
所有条目在构建时合并,用 ", " 连接写入 Depends 字段。
支持 Debian 版本约束语法:libfoo (>= 1.2)、libfoo (= 1.2.3) 等。
-->
<Dependency Include="libssl3" />
<Dependency Include="python3 (>= 3.10)" />
<!--
Condition 属性允许按 suite/arch 选择不同的依赖版本。
常见场景:同一个库在不同 suite 里包名或 SONAME 不同。
可用变量完整列表见上文 ItemGroup 条件语法说明(§ItemGroup 条目类型开头),
常用:$(Suite)、$(Arch)、$(Distro)。字符串比较需加单引号。
-->
<Dependency Include="libssl3" Condition="'$(Suite)' == 'resolute'" />
<Dependency Include="libssl3t64" Condition="'$(Suite)' == 'questing'" />Recommends 和 Suggests 直接作为 <PropertyGroup> 里的字符串填写,格式与 Depends 相同(逗号分隔,支持版本约束)。
<!--
Recommends:强烈推荐,但非必须。
- apt install 默认一并安装(除非用 --no-install-recommends)
- apt remove 单独卸载推荐包时不会破坏当前包的依赖
- 典型用途:元包(meta-package)列出它所代表的所有组件
-->
<Recommends>gnome-shell-extension-blur-my-shell, gnome-shell-extension-arcmenu</Recommends>
<!--
Suggests:可选的锦上添花。
- apt 不自动安装,仅作文字提示
- 典型用途:列出可以增强此包功能但完全独立的工具
-->
<Suggests>gnome-tweaks</Suggests>元包模式:创建一个没有任何
IncludeFile/IncludeFolder的包,全部内容只有Recommends, 即可实现类似ubuntu-desktop的元包语义——安装它会拉入一组软件,但可以单独卸载其中任意一个而不报依赖错误。
<!--
安装一个 systemd .service 文件,并自动生成完整的 maintainer scripts:
postinst (configure, 新装) → systemctl enable + systemctl start
postinst (configure, 升级) → systemctl try-restart
prerm (remove) → systemctl stop
postrm (remove/purge) → systemctl disable + daemon-reload
无需手写任何 maintainer scripts。
Include = .service 文件相对于 .aosproj 的路径
AutoEnable = true(默认)表示安装后立即 enable+start;
false 表示只安装 unit 文件,不自动启动
Mode = 可选,Unix 权限(3 位八进制字符串,如 "644")。
不设时默认为 644(不可执行)。
-->
<SystemdUnit Include="service/my-daemon.service" AutoEnable="true" />声明 dpkg trigger 关注(interest)或激活(activate)指令,每个条目对应生成的 DEBIAN/triggers 文件中的一行。dpkg 会在满足触发条件时自动调用此包的 postinst 或由其它包的触发器激活此包。
<!--
声明一个 trigger interest,表示此包关注指定路径的变化。
dpkg 会在目标路径被其他包修改后,自动触发此包的 postinst 脚本。
Include = 触发器名称,通常是一个文件系统路径(如 /etc/dconf/db)。
Type = 触发器指令类型,可选值:
• "interest" — 声明对此路径的兴趣(阻塞等待处理)
• "interest-noawait" — 声明兴趣但不阻塞(默认值)
• "activate" — 激活另一个包的触发器(阻塞等待处理)
• "activate-noawait" — 激活但不阻塞
不设 Type 时默认为 "interest-noawait"。
Condition = 可选,MSBuild 风格条件过滤,只在满足条件时才加入 triggers 文件。
-->
<DpkgTrigger Include="/etc/dconf/db" Type="interest-noawait" />
<!-- 多个触发器的典型用法示例 -->
<DpkgTrigger Include="/etc/dconf/db" Type="interest-noawait" />
<DpkgTrigger Include="/usr/share/glib-2.0/schemas" Type="interest" />
<DpkgTrigger Include="/etc/ld.so.conf.d" Type="activate-noawait" />生成的 DEBIAN/triggers 文件内容(每行一个 Type Name):
interest-noawait /etc/dconf/db
interest /usr/share/glib-2.0/schemas
activate-noawait /etc/ld.so.conf.d
<!--
在打 deb 之前执行的 shell 命令,适合编译步骤或资源生成。
执行顺序:上游解包(如果配置了 UpstreamSource)→ PrebuildCommand → IncludeFile 等文件覆盖。
因此可以在 PrebuildCommand 中对上游文件执行 sed、patch 等操作,再让本地文件覆盖最终版本。
Run = 要执行的命令(在 .aosproj 所在目录下执行)
-->
<PrebuildCommand Run="make release" />
<!-- 带条件:只在 amd64 上执行特定编译步骤 -->
<PrebuildCommand Run="make amd64-extras" Condition="'$(Arch)' == 'amd64'" />
<!-- 上游派生模式下修改上游文件 -->
<PrebuildCommand Run="sed -i 's/Ubuntu/AnduinOS/g' obj/$(Suite)_$(Arch)/etc/issue" /><!--
安装后由 dpkg 执行的自定义 shell 脚本(DEBIAN/postinst)。
适合 systemd 之外的初始化操作(如创建用户、设置权限、初始化数据库)。
多个条目按声明顺序追加到同一个 postinst 脚本中。
-->
<PostInstallScript Include="scripts/postinst.sh" /><!--
卸载前由 dpkg 执行的自定义 shell 脚本(DEBIAN/prerm)。
适合服务停止之外需要额外清理的操作。
-->
<PreRemoveScript Include="scripts/prerm.sh" /><!--
安装前由 dpkg 执行的自定义 shell 脚本(DEBIAN/preinst)。
适合安装前需要的准备工作(如备份旧数据、检查先决条件)。
多个条目按声明顺序追加到同一个 preinst 脚本中。
-->
<PreInstallScript Include="scripts/preinst.sh" /><!--
卸载后由 dpkg 执行的自定义 shell 脚本(DEBIAN/postrm)。
适合卸载后的残留清理操作(如删除日志、清理缓存)。
systemd unit 的 disable + daemon-reload 会在自定义脚本之后自动追加。
-->
<PostRemoveScript Include="scripts/postrm.sh" /><Project Sdk="Aiursoft.Apkg.Sdk">
<PropertyGroup>
<PackageName>anduinos-shell-ext</PackageName>
<PackageVersion>2.1.0</PackageVersion>
<PackageAuthors>AnduinOS Team <dev@anduinos.com></PackageAuthors>
<PackageDescription>AnduinOS GNOME Shell extensions</PackageDescription>
<PackageHomepage>https://anduinos.com</PackageHomepage>
<LicenseType>GPL-2.0</LicenseType>
<!-- 目标发行版:anduinos(对应服务器上配置的仓库 Distro 名) -->
<TargetDistro>anduinos</TargetDistro>
<!--
同时发布到两个 suite。
apkg build --all 会产出 2 suites × 2 arches = 4 个 .deb 文件。
-->
<TargetSuites>resolute questing</TargetSuites>
<TargetArchitectures>amd64 arm64</TargetArchitectures>
<Component>main</Component>
</PropertyGroup>
<!-- 预构建步骤:生成资源文件 -->
<ItemGroup>
<PrebuildCommand Run="make assets" />
</ItemGroup>
<ItemGroup>
<!--
可执行工具,安装后可在终端直接运行 anduinos-shell-ext。
IncludeScript 自动设置 0755 权限。
-->
<IncludeScript Include="bin/shell-ext" Target="/usr/bin/anduinos-shell-ext" />
<!--
GNOME Shell 扩展文件夹,整棵目录树都安装进去。
安装后结构:/usr/share/gnome-shell/extensions/shell-ext@anduinos/
├── extension.js
├── metadata.json
└── ...
-->
<IncludeFolder Include="extension/" Target="/usr/share/gnome-shell/extensions/shell-ext@anduinos" />
<!--
用户可编辑的配置文件。写入 conffiles,升级时 dpkg 会保护用户改动。
-->
<ConfFile Include="config/shell-ext.conf" Target="/etc/anduinos/shell-ext.conf" />
</ItemGroup>
<ItemGroup>
<!-- 所有 suite 共同依赖 -->
<Dependency Include="gnome-shell (>= 42)" />
<Dependency Include="gir1.2-glib-2.0" />
<!--
libssl 在 resolute 和 questing 里包名不同(SONAME 变更),
用 Condition 按 suite 分别声明。
-->
<Dependency Include="libssl3" Condition="'$(Suite)' == 'resolute'" />
<Dependency Include="libssl3t64" Condition="'$(Suite)' == 'questing'" />
</ItemGroup>
<ItemGroup>
<!--
后台辅助守护进程的 systemd unit。
apkg build 自动生成 postinst/prerm/postrm,无需手写维护脚本。
-->
<SystemdUnit Include="service/shell-ext-helper.service" AutoEnable="true" />
</ItemGroup>
</Project>apkg build --all 对上述文件产出的构建矩阵:
resolute × amd64 → bin/anduinos-shell-ext_2.1.0_resolute_amd64.deb
resolute × arm64 → bin/anduinos-shell-ext_2.1.0_resolute_arm64.deb
questing × amd64 → bin/anduinos-shell-ext_2.1.0_questing_amd64.deb
questing × arm64 → bin/anduinos-shell-ext_2.1.0_questing_arm64.deb
resolute 的 amd64 包 Depends 字段将是:
gnome-shell (>= 42), gir1.2-glib-2.0, libssl3
questing 的 amd64 包 Depends 字段将是:
gnome-shell (>= 42), gir1.2-glib-2.0, libssl3t64
manifest.xml 是 apkg publish 自动生成、嵌入 .apkg 归档的分发清单,不需要手写。Apkg 服务器读取它来决定把哪个 .deb 放进哪个 APT 仓库。
一个 .apkg 包包含两类属性:
| 类别 | 属性 | 位置 | 语义 |
|---|---|---|---|
| 死属性 | Name, Distro, Component |
manifest 根 | 唯一确定一个 APKG 包。三要素一旦设定即不可变。改变任意一个即为全新包。 |
| 活属性 | Version, Suite, Architecture |
manifest Entry | 每次上传可混合。一个 apkg 内可包含多个 (Version, Suite, Arch) 组合的 .deb 文件。 |
- Version 不在 manifest XML 中。服务端直接从
.deb文件内部解析版本号 — deb 本身是版本号的唯一真实来源。 - 所有 Entry 共享同一个根层的
Distro和Component。一个 apkg 只面向一个发行版的一个组件。 (Name, Distro, Component)三元组在数据库中全局唯一。第一个上传该三元组的用户拥有其所有权,其他用户上传相同三元组会被拒绝(403)。
<?xml version="1.0" encoding="utf-8"?>
<ApkgPackage FormatVersion="2">
<!-- ═══ 3 个死属性 — 包的唯一身份 ═══ -->
<Name>anduinos-shell-ext</Name>
<Distro>anduinos</Distro>
<Component>main</Component>
<!-- 包元信息(从 .aosproj PropertyGroup 映射) -->
<Maintainer>AnduinOS Team <dev@anduinos.com></Maintainer>
<Description>AnduinOS GNOME Shell extensions</Description>
<Homepage>https://anduinos.com</Homepage>
<License>GPL-2.0</License>
<!--
Entries — 构建矩阵展开结果。
每个 Entry = 一个 (Suite, Architecture) 组合 = 一个 .deb 文件。
服务端按 (Distro, Suite, Architecture) 三元组定位目标仓库。
Version 从 .deb 文件内部解析,不出现在 manifest 中。
-->
<Entries>
<Entry>
<DebFile>anduinos-shell-ext_2.1.0_resolute_amd64.deb</DebFile>
<Suite>resolute</Suite>
<Architecture>amd64</Architecture>
</Entry>
<Entry>
<DebFile>anduinos-shell-ext_2.1.0_resolute_arm64.deb</DebFile>
<Suite>resolute</Suite>
<Architecture>arm64</Architecture>
</Entry>
<Entry>
<DebFile>anduinos-shell-ext_2.1.0_questing_amd64.deb</DebFile>
<Suite>questing</Suite>
<Architecture>amd64</Architecture>
</Entry>
<Entry>
<DebFile>anduinos-shell-ext_2.1.0_questing_arm64.deb</DebFile>
<Suite>questing</Suite>
<Architecture>arm64</Architecture>
</Entry>
</Entries>
</ApkgPackage>| 字段 | 来源(.aosproj) |
说明 |
|---|---|---|
FormatVersion |
固定 2 |
格式版本标识 |
Name |
PackageName |
包名。三元组之一,不可变。 |
Distro |
TargetDistro |
目标发行版。三元组之一,不可变。 |
Component |
Component |
APT 组件。三元组之一,不可变。 |
Maintainer |
Maintainer 或 PackageAuthors |
维护者 |
Description |
PackageDescription |
简介 |
Homepage |
PackageHomepage |
主页 |
License |
LicenseType |
许可证 |
| 字段 | 说明 |
|---|---|
DebFile |
.apkg 归档内 .deb 文件名。命名规则:{Name}_{Version}_{Suite}_{Arch}.deb |
Suite |
APT suite 名(如 resolute、questing) |
Architecture |
CPU 架构(amd64、arm64、all)。all 匹配任意架构仓库。 |
服务端根据 (Distro, Suite, Architecture) 元组定位目标仓库(Distro 来自根节点,Suite 和 Architecture 来自 Entry)。找不到匹配仓库时跳过该 Entry 并记录警告。
⚠️ 静默跳过陷阱:apkg push返回成功不代表所有.deb都进了仓库。如果服务器上没有配置匹配的仓库,对应的.deb会被静默丢弃,打包者不会收到任何错误提示。
Apkg 的 push 流程涉及两套互相独立的三元组,它们各司其职但只有一个交集字段 Distro:
| 层面 | 三元组 | 作用 | 失败后果 |
|---|---|---|---|
| ApkgPackage 身份 | (Name, Distro, Component) |
确定"这是谁的包"、归属哪个用户、Revision 往哪累积 | 显式错误(403 所有权冲突、manifest 格式错误) |
| APT 仓库路由 | (Distro, Suite, Architecture) |
决定 .deb 文件落入哪个 AptRepository |
静默跳过(仅服务端日志留一行 Warning) |
注意两个三元组的字段是不同的:
- 身份三元组包含
Component,不含Architecture - 路由三元组包含
Architecture,不含Component
唯一的交集是 Distro。所以即使你的 Distro 写对了(ApkgPackage 身份正常创建),只要 Suite 或 Architecture 与服务器上的仓库配置不一致,对应的 .deb 就会静默消失。
假设你写了这样的 .aosproj:
<PackageName>my-tool</PackageName>
<TargetDistro>anduinos</TargetDistro>
<Component>main</Component>
<TargetSuites>noble-addon questing-addon</TargetSuites>
<TargetArchitectures>amd64</TargetArchitectures>apkg push 时,身份三元组 (my-tool, anduinos, main) 正常命中 ApkgPackage。然后两个 Entry 分别路由:
Entry 1: (anduinos, noble-addon, amd64) → 服务器上有这个仓库 ✅ → .deb 入库
Entry 2: (anduinos, questing-addon, amd64) → 服务器上没有这个仓库 ❌ → 跳过,丢弃
push 返回 200。你以为 questing 用户也能 apt install my-tool,实际上那个 .deb 已经被丢弃了。下次管理员创建 questing-addon 仓库后,你必须重新 push 一次——已丢弃的 .deb 不会自动恢复。
与 NuGet 的类比:NuGet 服务器原样存储整个 .nupkg,TFM 匹配发生在客户端。如果管理员没配
net10.0feed,你以后配了就行,不需要重新上传。Apkg 不同——Suite/Architecture 路由发生在服务端 push 时,没匹配上就直接丢弃。这不是 Bug,而是 APT 仓库架构决定的:每个(Distro, Suite, Architecture)组合对应一个独立的仓库端点,服务端必须在 push 时决定 deb 存到哪。
- Push 前先确认服务器上存在哪些仓库。联系你的服务器管理员,确认你的
TargetDistro、TargetSuites、TargetArchitectures对应的仓库都已创建。 - Push 后验证。在目标机器上执行
apt-cache show <你的包名>确认包是否在各个 suite 下都可见。如果只在一个 suite 下能看到,说明其他的被丢弃了。 - 问题不是你的
.aosproj写错了——是服务器侧配置不完整。责任在仓库管理员,但你需要主动推动他们对齐配置。
- 仓库创建应当与包发布者的声明对齐。如果你知道有包声明了
TargetSuites="noble-addon questing-addon resolute-addon",就应当创建三个对应的AptRepository,分别覆盖这三个 suite。 - 在 Web UI 或文档中公开你的仓库矩阵。让包发布者能查到:
- 支持的 Distro 列表
- 每个 Distro 下的 Suite 列表
- 每个 Suite 支持的 Architecture 列表
- 监控服务端日志。
"No repository found for (Distro=X, Suite=Y, Arch=Z)"这类 Warning 意味着有包发布者的.deb被丢弃了——这通常是你需要新建仓库的信号。 - Component 不参与路由。一个 AptRepository 的 Component 列表(如
main restricted universe)决定了该仓库包含哪些组件,但不影响路由匹配。路由只看(Distro, Suite, Architecture)。
💡 最佳实践:服务器管理员和包发布者应共享一份"支持的构建矩阵"文档。例如 AnduinOS 的官方矩阵是:
Distro Suite Architectures anduinos noble-addon amd64, arm64 anduinos questing-addon amd64, arm64 anduinos resolute-addon amd64, arm64 包发布者只声明这张表里存在的
(Distro, Suite, Arch)组合,管理员保证所有声明的组合都有对应的仓库。两边对齐就不会有静默丢失。
排查方法:检查服务器日志中的 "No repository found" 警告,或在目标机器上执行 apt-cache show <pkgname> 确认包是否可见。
与 Suite/Arch 的路由问题不同,Component 面临的是另一个维度的陷阱。如果你改了 .aosproj 的 <Component>,想把同一个包推到同一个仓库的不同组件,第二次 push 会被服务端 409 拒绝。
| 层面 | 实体 | Component 是否在唯一键中 | 效果 |
|---|---|---|---|
| ApkgPackage 身份 | ApkgPackage |
是 — (Name, Distro, Component) |
改 Component → 全新包家族 ✅ |
| LocalPackage 去重 | ApkgDebPackage |
否 — (RepositoryId, Package, Version, Architecture) |
改 Component → 仍然冲突 ❌ |
| APT 输出 | AptPackage |
是 — (Package, Version, Architecture, Component) |
到不了这一层 |
ApkgPackage 层会正确识别为新包,但 DebUploadService 的 slot conflict 检查用的是 (RepositoryId, Package, Version, Architecture) —— Component 不在其中。只要这四个字段相同,无论 Component 改成了什么,第二次 push 都会返回 409 Conflict。
同一个仓库、同一个 (Distro, Suite, Architecture) 组合下,同一个包以相同版本出现在两个 Component 中在 APT 协议层面会造成混淆——APT 客户端看到两份完全相同的条目,Pin-Priority 无法区分。Apkg 的设计选择是"一个包只属于一个 Component",这个限制是有意为之。
- 改 Component 意味着换包的身份。新的 Component 会创建新的
ApkgPackage家族(可能由不同用户拥有),这是合法的。 - 但如果目标仓库已经有同 (Package, Version, Architecture) 的包——不管它是哪个 Component 来的——push 会被拒绝。
- 如果你确实需要跨 Component 迁移:先在服务器上 disable 旧 Component 下的 LocalPackage,再用新 Component 重新 push。
- 409 Conflict 不是你的服务器配置问题——是包发布者的 push 内容与已有数据冲突。
- 如果你在日志中看到 409,告诉包发布者先 disable 旧包再重试,而不是让你在服务器侧做什么。
apkg new → 创建 .aosproj 骨架
apkg add → 往 .aosproj 追加文件条目
apkg lint → 验证 .aosproj 语法和文件存在性(见上方规则表)
apkg build → 默认构建全部 TargetSuites × TargetArchitectures;--suite/--arch 限定单个目标
apkg publish → 自动 lint + build(默认全部 target)+ 打包为 bin/<name>.apkg;--no-build 跳过构建
apkg push → 上传 .apkg 到 Apkg 服务器
apkg install → 从本地 .apkg 解出匹配当前系统的 .deb 并执行 dpkg -i
apkg add-source → 在当前机器添加 Apkg APT 源到 /etc/apt/sources.list.d/
apkg unpack → 解包 .apkg 归档(自动选择匹配当前系统的 .deb)
TargetSuites × TargetArchitectures 的笛卡尔积产出所有 .deb:
TargetSuites: resolute questing
TargetArchitectures: amd64 arm64
产出:
bin/pkg_1.0.0_resolute_amd64.deb
bin/pkg_1.0.0_resolute_arm64.deb
bin/pkg_1.0.0_questing_amd64.deb
bin/pkg_1.0.0_questing_arm64.deb
构建时会在 obj/<suite>_<arch>/ 下生成临时暂存目录(含 DEBIAN/ 控制文件和所有已复制的载荷文件),然后调用 dpkg-deb --build。构建失败时可在此目录检查生成内容,正常完成后可安全删除。
不带参数默认构建完整矩阵(等同于 --all);使用 --suite / --arch 限定单个目标。
apkg push bin/anduinos-shell-ext.apkg \
--source https://apkg-dev.aiursoft.com \
--api-key <你的 API Key>