相信经常使用 SPM 的小伙伴,应该都遇到过使用 Reset Package Caches 时报错:An unknown error occurred. reference 'refs/remotes/origin/main' not found (-1),或者其他分支。

关于这个问题在 stackoverflow 上有比较多的讨论,例如这个 SPM unknown error reference not found when changing branch

思来想去决定根据上面的回答,记录一下解决方案。

在我的场景下(Xcode 15),仅仅删除 ~/Library/Caches/org.swift.swiftpm/repositories 下对应的目录即可解决问题,之后重新 Rest 即可。

问题的发生

链接中提到了两种观点。一开始 @mrwest09 提到,如果使用 ssh(git@)引入依赖,则有可能会产生该问题。实际上这也符合我的使用场景(不过我没有试 https)。

但是后来 @Ivan Vavilov 也提到,他使用的 https,但是也依然出现了该问题。

所以怎么说好呢,这个问题的发生未必可以都归结到 ssh,至于真正的原因仅从上述回答中可能暂时无从得知...

问题的解决

直接方式

删除 ~/Library/Caches/org.swift.swiftpm/repositories 下的内容即可。

除此之外,回答中还提到了一个 “Swift package caches both in the derived data directory of your project”。该缓存的实际目录在 ~/Library/Developer/Xcode/DerivedData/$project/SourcePackages/repositories
我的场景下仅删除 org.swift.swiftpm 中的内容即可。如果你仅删除它无效,可以看再试试 一并 SourcePackages 下的内容。

自动化处理

回答中还有好心的大佬写了2个自动删除缓存的脚本,一个是:

#!/bin/bash

if [[ $# -eq 0 ]] ; then
    echo 'Please call the script with the name of your project as it appears in the derived data directory. Case-insensitive.'
    echo 'For example: ./fix-spm-cache.sh myproject'
    exit 0
fi

# Delete all directories named "remotes" from the global Swift Package Manager cache.
cd ~/Library/Caches/org.swift.swiftpm/repositories

for i in $(find . -name "remotes" -type d); do
    echo "Deleting $i"
    rm -rf $i
done

# Find derived data directories for all projects matching the script argument, and
# delete all directories named "remotes" from source package repositories cache for those projects. 

cd ~/Library/Developer/Xcode/DerivedData/

for project in $(find . -iname "$1*" -type d -maxdepth 1); do
    for i in $(find "$project/SourcePackages/repositories" -name "remotes" -type d); do
        echo "Deleting $i"
        rm -rf $i
    done
done

这个脚本删除了上面提到的两个路径下的缓存,使用时需要 sh ./fix-spm-cache.sh myproject

另外一个大佬使用了另外一个思路,编写了一个 python 脚本:

# Sometimes Xcode cannot resolve SPM(File -> Packages -> Resolve Package versions) if the dependency url is ssh
# This script is a workaround to resolve package versions.
# Usage:
#     python spmResolve.py
# or
#     python3 spmResolve.py
import os.path
import subprocess
import glob
import json


def main():
    package_file = "xcshareddata/swiftpm/Package.resolved"
    xcodeproj = glob.glob('*.xcodeproj')
    xcworkspace = glob.glob('*.xcworkspace')
    spmproj = glob.glob('Package.resolved')
    package_resolved = ""
    if xcodeproj:
        package_resolved = xcodeproj[0] + f"/project.xcworkspace/{package_file}"
    elif xcworkspace:
        package_resolved = xcworkspace[0] + f"/{package_file}"
    elif spmproj:
        package_resolved = spmproj[0]
    else:
        print(f"😱 Cannot find *.xcodeproj, *.xcworkspace or Package.resolved file")
        exit(-1)

    update_package_resolved(package_resolved)


def update_package_resolved(package_resolved):
    if not os.path.exists(package_resolved):
        print(f"😱 Package.resolved file doesn't exit: {package_resolved}")
        exit(-1)

    print(f"Found: {package_resolved}")
    f = open(package_resolved)
    content = json.load(f)
    f.close()
    for pin in content["pins"]:
        url = pin["location"]
        if "branch" in pin["state"]:
            branch = pin["state"]["branch"]
            commit_hash = get_git_revision_hash(url, branch)
            print(f"{url}, {branch}, {commit_hash}")
            pin["state"]["revision"] = commit_hash
        elif "version" in pin["state"]:
            version = pin["state"]["version"]
            commit_hash = get_git_revision_by_tag(url, version)
            print(f"{url}, {version}, {commit_hash}")
            pin["state"]["revision"] = commit_hash

    with open(package_resolved, "w") as output:
        json.dump(content,  output, indent=4)

    # resolve SPM
    subprocess.run(['xcodebuild', '-resolvePackageDependencies'])
    print('🎉 Well done')


def get_git_revision_hash(url, branch) -> str:
    command = f'git ls-remote {url} refs/heads/{branch} | cut -f 1'
    return get_git_command_output(command)


def get_git_revision_by_tag(url, version) -> str:
    command = f'git ls-remote {url} -t {version} | cut -f 1'
    return get_git_command_output(command)


def get_git_command_output(command) -> str:
    return subprocess.check_output(command, stderr=subprocess.STDOUT, shell=True).decode('ascii').rstrip()


if __name__ == '__main__':
    main()

使用方式为在包含 *.xcodeproj*.xcworkspace 文件的路径下,调用 python spmResolve.py