
在 Linux 圈子里混久了你会发现「repo」这个词是个典型的「一词多义陷阱」。刚入行的朋友问我「repo 怎么装」我第一反应往往是反问一句你说的是 Google 那套管 Android 源码的 repo 工具还是 yum、apt 背后那个软件源仓库这两个东西除了英文缩写长得一样从实现原理到使用场景几乎没有任何关系但它们在日常运维和开发中的出场率都极高而且都会以各自的方式把人坑到怀疑人生。这篇就把这两条线彻底拆开讲清楚一条是 repo 工具的下载、初始化、同步和日常维护另一条是 yum/dnf/apt 软件源仓库的配置逻辑与排错思路顺带把那些年我们踩过的cannot find a valid baseurl for repo、GPG 校验失败、sync 中断之类的经典坑一并交代明白。不管你是刚在虚拟机里装完第一个 Linux 想配个源还是准备拉一套嵌入式 BSP 或者 Android 源码这里面的内容都能直接用上。1. 先把概念掰开Linux 里的 repo 到底指什么很多人第一次接触 repo 这个概念是在搜「Linux repo 下载」的时候结果搜出来一堆 Android 源码编译教程越看越懵。原因就在于中文社区把两个完全不同的东西都简称成了 repo。搞清楚这一点后面的所有操作都会顺理成章。1.1 repo 工具Android 源码的多仓库管理利器repo 工具是当年为了管理 Android 这种「几百个 git 仓库组成一个超大项目」的场景而造出来的。要知道AOSP 的代码量放在单个 git 仓库里是不现实的编译一次要拉几百 GB任何一个 commit 都会让仓库体积爆炸。所以 Android 的做法是把系统拆成上千个独立的 git 仓库再用一个 manifest XML 文件把这些仓库的地址、分支、路径全部描述出来repo 工具就是用来读这个 manifest 并批量操作这些仓库的。它的定位非常清晰——repo 不是 git 的替代品而是 git 的批处理调度器。你执行repo sync它实际上是遍历 manifest 里的每一个 project对每个 project 调一次git fetch加git checkout你执行repo forall -c 命令它就是对每个仓库循环执行同一个命令。理解这一点很关键因为这解释了为什么 repo 的很多行为看起来「不像是 git 干的」比如它的分支切换是批量改 manifest 的 revision而不是逐个仓库git checkout。从源码结构上看repo 本身是一个 Python 脚本主程序在.repo/repo/目录下它还会顺带把 git 的一些辅助脚本比如git-repo子命令挂进去。你在命令行敲的repo执行的其实是这个 Python 脚本入口脚本再去调真正的 git 二进制。这也意味着如果你的 Python 环境有问题repo 是跑不起来的报错会非常莫名其妙。1.2 软件源仓库包管理器背后真正干活的角色另一条线是软件源仓库也就是系统里/etc/yum.repos.d/下那些.repo文件或者sources.list里那些地址。这里的 repo 是 repository 的缩写指的是「一堆 RPM 或 DEB 包加上索引元数据」的集合包管理器从里面查找、下载、校验并安装软件。它的工作机制是这样的仓库目录里除了实际的包文件还有一份元数据文件RPM 系是repodata/repomd.xml及其下挂的 primary、filelists、other 等压缩索引DEB 系是Packages.gz和Release。包管理器在本地缓存这份元数据当你执行yum install或者apt install时它先查本地缓存解决依赖关系确定需要下载哪些包再从仓库地址拉取。所以yum makecache或者apt update这个动作本质就是刷新本地元数据缓存。2. repo 工具从下载到跑通全流程回到 Android 那条线。repo 工具本身不在系统包仓库里得手动下载这也是很多新人的第一个坎。整个过程其实只有四步拿启动器、加执行权限、初始化、同步。但每一步都有细节。2.1 下载 repo 启动器与 PATH 配置官方推荐的下载方式是直接取那个 Python 脚本。考虑到访问速度我一般直接用国内镜像mkdir -p ~/bin curl -sL https://mirrors.tuna.tsinghua.edu.cn/git/git-repo -o ~/bin/repo chmod ax ~/bin/repo这里有几个容易忽略的点。第一~/bin这个目录不一定在你当前的 PATH 里很多发行版默认只把/usr/local/bin加进去了。你可以echo $PATH看一下如果没有就在~/.bashrc或者~/.zshrc末尾补一句export PATH$HOME/bin:$PATH然后source ~/.bashrc生效。第二别用sudo把 repo 丢到/usr/bin因为 repo 依赖用户级配置和 SSH 凭据混用 root 和普通用户身份会让后面 sync 时的凭据缓存彻底乱套。第三如果你之前装过旧版 repo~/.repoconfig/里可能残留老的配置遇到诡异问题时可以先备份这个目录再重试。注意下载完成后务必核对文件能否执行repo --version能打印版本号才算装好。如果报No such file or directory八成是 shebang 里的 Python 解释器路径不对用head -1 ~/bin/repo看看指向哪里。2.2 repo init读懂 manifest 才是关键初始化这一步是整个流程里信息量最大的repo init -u https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/platform/manifest -b android-14.0.0_r1-u指定 manifest 仓库的地址-b指定分支或 tag。执行之后repo 会在当前目录创建.repo/目录里面至少包含这几样东西repo/repo 工具自身代码、manifests/manifest 仓库的克隆、manifest.xml指向当前生效的那个 manifest 文件以及后续 sync 下来的各个项目仓库。manifest 文件长这样manifest remote nameaosp fetchhttps://android.googlesource.com/ reviewandroid-review.googlesource.com / default revisionrefs/tags/android-14.0.0_r1 remoteaosp sync-j4 / project pathbuild/make nameplatform/build groupspdk / project pathframeworks/base nameplatform/frameworks/base groupspdk / /manifestremote定义远程地址前缀default定义默认 revision 和并发数每个project描述一个子仓库的路径和名称。你想改源、加仓库、排除某个模块全部动这个文件。但注意直接改.repo/manifests/里的文件会被下次repo init覆盖正确做法是放到.repo/local_manifests/目录下自己新建一个xxx.xmlrepo 会自动合并进去。另外-b后面的分支名建议直接用官方 tag比如android-14.0.0_r1因为分支名会随 HEAD 移动今天同步和三个月后同步拿到的代码可能完全不一样编译问题排查起来非常痛苦。2.3 repo sync参数怎么配才不折腾同步阶段参数最讲究配不好就是几小时起步的等待repo sync -c -j8 --no-tags --no-clone-bundle --fail-fast逐个解释。-c表示只同步当前分支不拉所有分支的引用能省掉大量带宽-j8是并发数经验值是 CPU 核心数或者网络带宽能撑住的上限虚拟机里给 4 就够物理机百兆带宽给 8 到 16 比较合适给太多反而因为磁盘随机读写把 IO 打满。--no-tags不拉取 tag 对象能显著减少小文件数量。--no-clone-bundle不去尝试 clone.bundle 这种打包文件国内网络环境下这个经常超时。--fail-fast遇到错误立即停止而不是傻乎乎地继续跑完整轮再告诉你失败早期排查的时候特别有用。还有个隐藏技巧--depth1可以做浅克隆只拉最近一次提交适合只关心编译不关心历史的场景能省下大量空间。但浅克隆在某些需要查历史的操作比如git log、repo diff上会受限而且后续想补全历史要git fetch --unshallow比较麻烦所以定位要清楚。2.4 repo 日常子命令速查同步完之后日常高频命令其实就那么几个我整理成表格方便对照命令作用使用场景repo sync批量拉取并更新所有项目首次同步、日常更新repo status查看所有仓库的改动状态提交前检查repo diff显示所有仓库的未提交差异代码 reviewrepo forall -c cmd在所有仓库执行同一命令批量切分支、清理repo start name .在所有仓库创建同名分支开发新特性repo upload批量提交到 Gerrit走审核流程repo prune清理已合并的本地分支定期维护repo manifest -r -o fixed.xml导出当前精确版本清单版本冻结、交付其中repo forall我个人用得最多。比如想一次性看所有仓库当前分支repo forall -c echo $REPO_PATH : $(git rev-parse --abbrev-ref HEAD)这里$REPO_PATH是 repo 注入的环境变量不用自己拼路径。想批量清理未跟踪文件也可以但务必先repo status确认一遍因为git clean -xdf会删掉所有未跟踪文件包括你辛苦编译出来的产物分类目录repo forall -c git clean -xdf3. 软件源仓库配置yum 与 apt 两条路线换了条线这里讲的 repo 才是日常运维碰得最多的。无论是刚装好的最小化系统还是内网自建的发布仓库核心逻辑都是一样的告诉包管理器去哪里找包、怎么校验、优先用哪个。3.1 yum/dnf 仓库文件的字段含义先看一个标准的.repo文件[base] nameCentOS-$releasever - Base - mirrors.aliyun.com baseurlhttps://mirrors.aliyun.com/centos/$releasever/os/$basearch/ enabled1 gpgcheck1 gpgkeyhttps://mirrors.aliyun.com/centos/RPM-GPG-KEY-CentOS-7 priority1字段逐个说清楚。方括号里的是仓库 ID全局唯一重复会报错。name只是给人看的描述。baseurl和mirrorlist二选一前者是固定地址后者是一个返回地址列表的 URL包管理器会自己挑一个可用的。enabled1表示启用。gpgcheck1开启签名校验这个千万别关关了以后任何被篡改的包都能装进来。priority是优先级数值越小优先级越高多仓库共存时用它解决版本冲突。有个细节值得展开$releasever、$basearch、$arch这些是变量由包管理器在运行时替换。$releasever通常取系统主版本号比如 CentOS 7 就是 7。这就是为什么换源时把mirror.centos.org换成阿里云地址$releasever不用改也能正常工作的原因。但如果你从 CentOS 换到别的衍生版$releasever可能会取到意外的值这时候要么手动写死版本号要么在/etc/dnf/vars/下定义覆盖变量。配置完记得刷缓存yum clean all yum makecache两条命令的顺序不能反。clean all会把旧的元数据和包缓存全部清掉makecache再重新下载。只执行后者的话旧缓存里的失效条目可能还在导致装了包却提示找不到。3.2 apt 的 sources.list 与 deb822 新格式Debian/Ubuntu 这边传统写法是/etc/apt/sources.listdeb https://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse deb https://mirrors.aliyun.com/ubuntu/ jammy-updates main restricted universe multiverse deb https://mirrors.aliyun.com/ubuntu/ jammy-security main restricted universe multiverse一行四个部分类型deb是二进制包deb-src是源码包、地址、发行版代号、组件列表。main是官方支持的自由软件restricted是官方支持的专有驱动universe是社区维护的自由软件multiverse是有版权或法律限制的软件。想少装东西就精简组件列表但缺组件会导致某些包死活装不上找半天才发现是源没开。新版 Ubuntu 推的是 deb822 格式文件后缀.sources放在/etc/apt/sources.list.d/Types: deb URIs: https://mirrors.aliyun.com/ubuntu/ Suites: jammy jammy-updates jammy-security Components: main restricted universe multiverse Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg这种格式可读性好也支持更细粒度的配置。迁移的时候注意一点如果同时存在sources.list和.sourcesapt 会把两份合起来读同一个源写两遍不会报错但会拖慢 update清理掉冗余是必要的。更新索引和查看来源apt update apt-cache policy nginxapt-cache policy会列出某个包在所有可用源里的版本和优先级排查「为什么装的是旧版本」这类问题时非常直接。3.3 密钥管理从 apt-key 到 signed-by包签名的密钥这块变化比较大。老教程里到处是apt-key add但这个命令在新版本里已经被标记废弃了原因是它把密钥加到全局信任区任何源都能用这个密钥验证安全性太差。现在的正确姿势是把密钥放到独立目录再通过signed-by绑定到具体某个源sudo mkdir -p /etc/apt/keyrings curl -fsSL https://example.com/repo-key.gpg | sudo gpg --dearmor -o /etc/apt/keyrings/example.gpg然后在 sources 文件里加上Signed-By: /etc/apt/keyrings/example.gpgRPM 系这边对应的是rpm --import导入的密钥会进入 RPM 数据库然后gpgcheck1时用它校验包签名。如果导入失败通常是密钥文件格式不对网上流传的很多 key 文件其实是 ASCII armored 格式带-----BEGIN PGP PUBLIC KEY BLOCK-----那种需要先gpg --dearmor转换才能被 RPM 识别。3.4 自建内网仓库的完整思路当你管理几十台机器、软件版本必须严格统一的时候内网自建仓库几乎是必然选择。整体链路是这样的用reposync把上游仓库的包镜像到本地目录用createrepo_c生成索引用 nginx 发布出去各机器配置指向内网地址的.repo文件。reposync -p /data/repo --repoidbase --download-metadata createrepo_c --update /data/repo/base--download-metadata这个参数很关键它会把上游的 repodata 一起拉下来比本地重新生成的索引更完整尤其对模块化modular包的处理更准确。增量更新的时候createrepo_c --update只处理变化的包速度能快很多。发布阶段 nginx 加一个 location 指过去就行注意把目录权限设成可读否则客户端会报 403 或者直接 baseurl 找不到。DEB 系可以考虑 aptly它额外提供了快照、包过滤、镜像合并的能力比裸目录发布灵活不少代价是多一层学习成本。4. 典型报错排查实录理论讲完了真正的功夫都在排错上。这部分我挑几个出现频率最高的报错把定位思路和解决路径讲透。4.1 cannot find a valid baseurl 到底是哪里出的问题这个报错几乎是每个用 CentOS 的人都遇到过Could not retrieve mirrorlist https://mirrorlist.centos.org/?release7archx86_64repoos error was 14: curl#6 - Could not resolve host: mirrorlist.centos.org Cannot find a valid baseurl for repo: base/7/x86_64看到这个先别急着改配置按顺序走三步定位。第一步cat /etc/resolv.conf看 DNS 配了没有ping 8.8.8.8和ping mirrors.aliyun.com分别测一下网络连通性和域名解析能 ping IP 不能 ping 域名就是 DNS 的事。第二步curl -I直接访问报错里那个地址看返回什么状态码403、404 和连接超时对应完全不同的原因。第三步yum repolist all看所有仓库的状态被禁用的会标disabled地址出错的会标status。如果是cannot find a valid baseurl for repo: base/7/x86_64这种而且还是 CentOS 7那基本可以确定是 CentOS 7 停止维护之后官方 mirrorlist 服务下线导致的。解决办法是把源全部指向 vault 或者国内镜像wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.repo sed -i s/mirror.centos.org/mirrors.aliyun.com/g /etc/yum.repos.d/CentOS-Base.repo yum clean all yum makecache至于cannot find a valid baseurl for repo: centos-sclo-rh/x86_64这是 SCLoSoftware Collections仓库的问题CentOS 7 后面 SCLo 也一并进了 vault需要单独把/etc/yum.repos.d/CentOS-SCLo-scl-rh.repo和CentOS-SCLo-scl.repo里的地址改成vault.centos.org/7.9.2009/sclo/$basearch/。改完之后yum repolist里 SCLo 那两行应该从红变绿。提示改源之前先备份原文件cp /etc/yum.repos.d/CentOS-Base.repo{,.bak}这种写法能省不少事。改完必须yum clean all再makecache跳过这一步经常会出现「明明改对了却还报旧地址」的假象。4.2 GPG 校验失败与依赖冲突GPG 校验失败的报错长这样Public key for xxx.rpm is not installed意思是包下下来了但校验用的公钥不在 RPM 数据库里。直接rpm --import keyurl导入对应仓库的 key 就行key 地址写在.repo文件的gpgkey字段里可以直接复制过来。还有一种情况是BADSIG或者NOKEY前者是签名和包内容对不上可能是因为中间有代理缓存了旧版本的文件清缓存重下基本能解决后者和上面一样就是缺 key。依赖冲突在家用场景见得少内网多仓库环境里很常见。比如 A 仓库和 B 仓库都提供同一个包的不同版本包管理器选了一个结果它的依赖要另一个版本就报冲突。这时候priority就派上用场了给主要仓库设优先级 1辅助仓库设 10 或者更高让选择变确定。dnf用户还可以用--best强制选择最优版本或者--allowerasing允许替换冲突包——但这个参数要慎用它可能删掉你不想删的东西。4.3 repo sync 中断与本地改动丢失Android 那边最让人抓狂的是 sync 跑到一半断了或者提示本地有改动不敢继续。先说中断。默认情况下 repo sync 是「尽力而为」某个仓库失败会继续跑后面的最后才汇总报错所以中途 CtrlC 之后状态可能是半拉子。这时候不要盲目重跑先看看哪些仓库没同步完repo status repo forall -c git status --short确认哪些仓库是干净的、哪些有残留之后对干净的直接重跑 sync 就行repo 本身有断点续传的能力已经拉好的仓库会跳过。对卡住的仓库可以单独进去git fetch --all git reset --hard origin/xxx。再说本地改动。repo sync 默认会在有未提交改动时拒绝更新报error: Your local changes would be overwritten。正确的处理方式是先把改动保存起来repo forall -c git stash push -m wip-before-sync repo sync -c -j8需要的时候再repo forall -c git stash pop恢复。别用--force-sync硬覆盖那个参数会把本地未提交的改动直接丢掉包括你可能改了三天还没提交的调试代码血的教训。4.4 中文乱码与环境变量那些事中文环境下的乱码问题通常出现在两个地方。一个是终端 locale 没配对repo或者git输出的中文提示变成问号检查locale命令的输出看看LANG、LC_ALL是不是C或者POSIX是的话在~/.bashrc里补上export LANGen_US.UTF-8 export LC_ALLen_US.UTF-8这里有个取舍用 UTF-8 是主流但如果你要处理 GBK 编码的历史文件可能得临时切到zh_CN.GBK。另一个地方是解压中文命名的压缩包时出现乱码尤其是 zip 格式因为 zip 规范里的文件名编码没有统一Windows 打的包经常是 GBK。Linux 下可以用unzip -O CP936 file.zip指定编码或者干脆换bsdtar它对编码的猜测比 unzip 聪明不少。7z 格式一般没这个问题7z x file.7z直接就能用前提是装了p7zip或者7zip包。至于 git 提交信息里的中文如果团队里有人用 Windows 有人用 Linux最好统一在仓库配置里声明编码git config --global i18n.commitEncoding utf-8 git config --global i18n.logOutputEncoding utf-8这样 log 输出会做编码转换不会出现一堆看不懂的十六进制。5. 实操心得与避坑清单前面讲的是「怎么做」这里讲讲「为什么这么做」以及「怎么不踩坑」。这些内容大部分是文档里不会写的全是从实际项目里磨出来的。5.1 权限、用户与目录规范一个反复出现的坑是混用 root 和普通用户。repo 工具在 sync 时会写.repo/目录下的凭据文件和缓存如果先用 root 跑了一次再用普通用户跑就会遇到权限拒绝而且报错信息往往很含糊。所以我的习惯是从头到尾只用普通用户操作需要安装编译依赖的时候才用 sudo。系统这边装依赖直接用 yum 或 apt不用 sudo 的只有 repo 目录的操作。用户和用户组这块新系统上来的第一件事就是确认自己在一个有 sudo 权限的用户下然后按需建开发账号sudo useradd -m -s /bin/bash dev sudo usermod -aG wheel dev-m是建 home 目录-s指定 shell-aG是追加到附加组。注意-a不能省否则会把你原来的组全顶掉。加完组后要重新登录才生效id dev确认一下。目录规范方面我的习惯是源码放在~/work/项目名/工具放在~/bin/日志和构建产物放在项目内的out/或者build/。这样备份的时候只需要打包~/work不会把几十 GB 的编译中间产物一起塞进去。5.2 镜像选择与网络调优镜像站挑哪个其实影响很大。阿里云、清华、中科大这几家的同步频率都不错但服务的地域侧重不同实际速度得自己测。我一般会同时测几个用curl -o /dev/null -s -w %{speed_download}\n输出下载速度选快的那个curl -o /dev/null -s -w %{speed_download}\n https://mirrors.aliyun.com/ubuntu/dists/jammy/Releaserepo 工具这边镜像选择是通过 manifest 里的fetch地址控制的改.repo/manifests/不持久所以要用local_manifests覆盖。还有一种做法是在~/.gitconfig里配全局的 url 替换规则[url https://mirrors.tuna.tsinghua.edu.cn/git/AOSP/] insteadOf https://android.googlesource.com/这样一来不管 manifest 里写的是哪个地址git 实际访问都会走镜像。这个技巧对所有 git 仓库都通用非常好使。并发数也不是越大越好。repo sync -j8在机械硬盘上往往比-j4还慢因为同时打开太多文件句柄导致寻道风暴。SSD 上可以放宽到-j16。判断标准是看系统负载和磁盘 IOiostat -x 1里%util长期贴着 100 就说明给多了。5.3 常见问题速查表把上面几节的内容压缩成一张表方便出问题时对着查报错/现象大概率原因处理方式cannot find a valid baseurl for repo: base/7/x86_64官方 mirrorlist 下线换 vault 或国内镜像改完yum clean all makecachecannot find a valid baseurl for repo: centos-sclo-rh/x86_64SCLo 仓库地址失效单独改 SCLo 两个 repo 文件指向 vaultCould not resolve hostDNS 配置错误检查/etc/resolv.conf先 ping IP 再 ping 域名Public key is not installed缺少 GPG 公钥rpm --import导入gpgkey里那个地址repo sync报本地改动会被覆盖有未提交修改repo forall -c git stash push后重试repo 提示 Python 版本不兼容系统 Python 太老升级 Python 或切到较新的发行版终端中文显示为问号locale 没设 UTF-8配置LANG/LC_ALL并重新登录装包提示依赖冲突多仓库版本打架用priority调优先级谨慎用--allowerasingapt 提示密钥已废弃用了老的apt-key迁移到signed-by独立密钥目录repo sync长期卡在某个仓库单个仓库网络或权限问题单独进目录git fetch看具体报错5.4 几个我认为最值钱的经验第一个是版本冻结。项目要交付的时候一定要repo manifest -r -o release.xml导出一份锁定版本号的清单把这个文件随代码一起归档。以后再想复现当时的构建环境用repo init -m release.xml就能精确还原比记住一堆分支名靠谱得多。这个动作花不了两分钟但能省掉未来无数次的「怎么编译不过了」。第二个是定期 prune。repo 目录用久了体积会膨胀得离谱因为每个子仓库里都堆着大量的远程分支引用和合并过的本地分支。定期跑repo forall -c git gc --auto repo prune能把体积压下来不少。特别是repo prune它会删掉已经合并进主干的本地分支很多人根本不知道有这条命令。第三个是保留一份离线仓库。内网或者网络不稳的环境下我会把常用的软件源同步到本地一台机器上用 nginx 发布成内网源所有测试机都指向它。这样即使外网断了新机器也能源码装依赖。这个投入一次后面几个月都省心。第四个是针对嵌入式场景的。做嵌入式 Linux 的时候交叉编译工具链和内核源码往往来自不同仓库用 repo 管理的前提是供应商提供了 manifest。如果没提供自己写一份也不难把 BSP、kernel、bootloader、rootfs 各自的 git 地址按project标签列进去就行。我自己维护的一套 manifest 用了三年换机器只要repo init加repo sync半小时就能搭好完整的开发环境比手动 clone 七八个仓库靠谱得多。最后说个冷门但实用的repo支持--no-repo-verify参数跳过自校验正常情况别用但在某些企业内网环境下 repo 工具的签名校验会因为证书链问题卡住临时加这个参数能救急。用完记得去掉毕竟自校验是安全防线的一部分。另外REPO_URL环境变量可以指定 repo 工具自身的更新源如果你在内网做了镜像把它设成内网地址repo init 时就不会去外网拉自己了。export REPO_URLhttps://mirrors.tuna.tsinghua.edu.cn/git/git-repo这条我在给团队配环境的时候必写进~/.bashrc能省掉每个人各自踩一遍下载慢的坑。