利用 iCloud Drive 来同步 Xcode 配置

2,119 阅读7分钟

前言

多年以前一位老程序员告诉笔者代码片段(code snippets)是程序员的财富,他有一个 U 盘,里面装着他的财富。每当他需要切换电脑写代码的时候,他就会把把精心配置的字体、主题、代码片段等部署到新电脑上,然后开始高速编码。每次看他写代码都是一种享受,不过这是另一个故事了。

需求

多年之后,笔者也终于凑够了钱买了自己的 Mac,闲暇无事的时候也会写写代码祭奠下逝去的青春。但是某些时候总会觉得很别扭,例如感觉字体和单位的有细小的差距,或者一个代码片段怎么也按不出来——最后发现是没有在这台电脑配置这段代码片段。这种事发生的事情多了之后,就会感觉厌烦,同样的操作为什么得重复两次、三次?或者拿出吃了几年灰的 U 盘抽插在各地的电脑上人工同步?就不能有什么办法可以一次更改多次应用?笔者稍微一拍脑门,想到了今天的主角——iCloud Drive

  1. 为什么使用 iCloud Drive?
    • 因为这是苹果本家的网盘,嵌入系统中,只要开启我们就无需关心上传下载,正如 OneDrive 在 Windows 一样,我们只需要把文件放进去,他就会自动开始上传,并在你的每一台苹果设备上同步。利用这点我们就能方便的做到在不同的设备上同步 Xcode 配置文件,无需手动同步或者上传下载。
  2. 其他的替代方案
    • GitHub 之类的大型同性交友网站
      • 目前想来用 git 应该更好更方便,不过实现起来有点复杂,有能力的朋友可以自己动手
    • OneDrive/坚果云等网盘
      • 我觉得能有自带的还是用自带的吧

思路

总所周知 Xcode 的代码片段是保存在~/Library/Developer/Xcode/UserData/CodeSnippets路径下的,附近位置还有主题等配置信息。基于笔者的经验我们只需要备份同级目录下的 CodeSnippets、FontAndColorThemes 和 KeyBindings 三个子目录就行了。每当我们修改了代码片段、主题或者快捷键,把对应的文件放在 iCloud Drive 同步,当在其他电脑上时就使用最新的覆盖到对应目录即可。

脚本

虽说思路如此,但是笔者肯定不敢把这种三岁小孩子就能分析出来的东西发出来糊弄人。所以为了简化这个繁琐而又机械的操作,笔者编写了这样一个脚本:

#!/usr/bin/env bash

set -euo pipefail

################# variable define ##########
now=`date "+%Y%m%d%H%M%S"`

red=`tput setaf 1`
green=`tput setaf 2`
yellow=`tput setaf 3`
reset=`tput sgr0`

xcode_dir="${HOME}/Library/Developer/Xcode/UserData"
cloud_backup_dir="${HOME}/Library/Mobile Documents/com~apple~CloudDocs/XcodeBackup"
local_backup_dir="${HOME}/资源/归档/XcodeBackup"

code_snippets="CodeSnippets"
font_and_color_themes="FontAndColorThemes"
key_bindings="KeyBindings"

########### MAIN ##################
# check directory exist
if [ ! -d "${cloud_backup_dir}" ]; then
    echo "${red}iCloud Drive备份路径不存在!${reset}"
    mkdir -p "${cloud_backup_dir}"
    echo "${green}自动创建iCloud Drive备份路径:${reset}${cloud_backup_dir}"
    else
    echo "${green}iCloud Drive备份路径:${reset}${cloud_backup_dir}"
fi

if [ ! -d "${local_backup_dir}" ]; then
    echo "${red}本地备份路径不存在!${reset}"
    mkdir -p "${local_backup_dir}"
    echo "${green}自动创建本地备份路径:${reset}${local_backup_dir}"
    else
    echo "${green}本地备份路径:${reset}${cloud_backup_dir}"
fi

# zip files
cd "${xcode_dir}"
zip -r "${cloud_backup_dir}/XcodeBackup+${now}.zip" "${code_snippets}" "${font_and_color_themes}" "${key_bindings}" &
zip -r "${local_backup_dir}/XcodeBackup+${now}.zip" "${code_snippets}" "${font_and_color_themes}" "${key_bindings}" &

wait

# delete unnecessary backup files
num=`ls -l "${cloud_backup_dir}" |grep "^-"|wc -l`
if [ ${num} -gt 5 ]; then
    num=`expr ${num} - 5`
    cd "${cloud_backup_dir}"
    ls -tr "${cloud_backup_dir}" | head -${num} | xargs rm
fi

num=`ls -l "${local_backup_dir}" |grep "^-"|wc -l`
if [ ${num} -gt 5 ]; then
    num=`expr ${num} - 5`
    cd "${local_backup_dir}"
    ls -tr "${local_backup_dir}" | head -${num} | xargs rm
fi

简化了这个繁琐的操作,仅需在开机的时候跑一下,就能达到自动备份的效果。功能也是十分的简单:

  1. 首先创建了两个备份 Xcode 配置文件的路径,一个在云端,一个在本地(本地路径大家可以自行配置,一般也不会用上)。
  2. 然后把 Xcode 归档到这两处各一份,笔者这里选用 zip 包而不是更高压缩比的 7zip 等是因为想做通用一点便于大家开箱即用,不需要额外安装其他软件。
  3. 最后将多次运行后生成的老包删除,只保留最新的 5 个,以便节约宝贵的空间(毕竟笔者比较穷只舍得用免费的 5g 版)

有了这个脚本之后,大家只需要坚持开机的时候跑一跑就行了。笔者喜欢每天开机就更新下 cocoapods、brew、brew cask 这类的,所以就写了个脚本,刚好顺便也就备份一下。脚本思路大致如下,因为和主题无关就不细说了。

#!/usr/bin/env bash

open 自用魔法丝袜之影

wait

pod repo update --verbose &
更新Homebrew cask &
备份各种币钱包 &

备份Xcode等IDE配置文件 &

wait

killall 自用魔法丝袜之影

不过这样其实也不是很方便,毕竟打开 terminal 输入指令都很烦了,难道还要手动计算这台电脑的配置是否是最新的?然后再考虑是不是需要把云盘里面的配置解压到指定的位置覆盖?而且很有可能在做这些前已经把这台电脑的配置当最新版上传到云盘里了。

让所有的电脑用同一个版本的配置

笔者再次进行了思考。如果可以根据这些文件的最后修改日期和备份的文件进行比较,谁新就用哪个版本,那么不就实现了吗?只要我们确保每次修改都跑一次脚本,每次开机都跑一次,就能达到我们想要的效果了。至于如何判断文件的最后修改时间,笔者认为只需要一个根据文件名生成的 key 和一个对应的文件的最后修改时间做 value 的数据结构就行了(虽说也可以把备份的文件展开比较,但是因为笔者才疏学浅,尚不知如何操作,就只能通过键值对来判断了) 不过实际操作起来,再次彰显了笔者的才疏学浅,笔者也不知道如何在 bash 中创建一个高效并能持久化的键值对,如果哪位大佬知道请务必告诉笔者。 最后笔者想到 Mac 自带的 SQLite3,虽说这样一个小小的功能上数据库是有一点高射炮打蚊子,但是能跑就行吧。脚本如下;

#!/usr/bin/env bash

set -euo pipefail

################# variable define ##########
now=`date "+%Y%m%d%H%M%S"`

red=`tput setaf 1`
green=`tput setaf 2`
yellow=`tput setaf 3`
reset=`tput sgr0`

xcode_dir="${HOME}/Library/Developer/Xcode/UserData"
cloud_backup_dir="${HOME}/Library/Mobile Documents/com~apple~CloudDocs/XcodeBackup"
local_backup_dir="${HOME}/资源/归档/XcodeBackup"

xcode_backup_database="${HOME}/Library/Mobile Documents/com~apple~CloudDocs/.BackupDatabase"

code_snippets="CodeSnippets"
font_and_color_themes="FontAndColorThemes"
key_bindings="KeyBindings"

temp="DoNotModify"
database="${xcode_backup_database}/${temp}"

########### MAIN ##################
# check directory exist
if [ ! -d "${cloud_backup_dir}" ]; then
    echo "${red}iCloud Drive备份路径不存在!${reset}"
    mkdir -p "${cloud_backup_dir}"
    echo "${green}自动创建iCloud Drive备份路径:${reset}${cloud_backup_dir}"
else
    echo "${green}iCloud Drive备份路径:${reset}${cloud_backup_dir}"
fi

if [ ! -d "${local_backup_dir}" ]; then
    echo "${red}本地备份路径不存在!${reset}"
    mkdir -p "${local_backup_dir}"
    echo "${green}自动创建本地备份路径:${reset}${local_backup_dir}"
else
    echo "${green}本地备份路径:${reset}${cloud_backup_dir}"
fi

if [ ! -d "${xcode_backup_database}" ]; then
    echo "${red}同步数据库路径不存在!${reset}"
    mkdir -p "${xcode_backup_database}"
    echo "${green}自动创建数据库路径:${reset}${local_backup_dir}"
else
    echo "${green}数据库路径:${reset}${cloud_backup_dir}"
fi

sqlite3 "${database}" 'create table if not exists backupXcode(id integer primary key not NULL,key integer unique not NULL,value integer not NULL);'

#获取最后修改时间
cd "${xcode_dir}"
if [ -d "./${code_snippets}" ]; then
    find "./${code_snippets}" -type f >> ${temp}
fi
if [ -d "./${font_and_color_themes}" ]; then
    find "./${font_and_color_themes}" -type f >> ${temp}
fi
if [ -d "./${key_bindings}" ]; then
    find "./${key_bindings}" -type f >> ${temp}
fi

while read path; do
    key=`md5 -q -s "${path}"`
    value=`stat -f "%m" "${path}"`
    isModify=`sqlite3 "${database}" "select value from backupXcode where key == '${key}';"`
    if [ -z ${isModify} ]; then
        echo "${yellow}本地Xcode配置尚未同步${reset}!"
        num=`ls -l "${cloud_backup_dir}" |grep "^-"|wc -l`
        if [ ${num} -ge 1 ]; then
            echo "${green}找到最新的Xcode配置,开始自动替换${reset}!"

            cd "${xcode_dir}"
            ## backup before
            zip -r "XcodeBackup.zip" "${code_snippets}" "${font_and_color_themes}" "${key_bindings}" &
            wait

            cd "${cloud_backup_dir}"
            newBackup=`ls -t | head -1`

            unzip -u "${newBackup}" -d "${xcode_dir}" &
            wait

            cd "${xcode_dir}"
            rm ${temp}
            if [ -d "./${code_snippets}" ]; then
                find "./${code_snippets}" -type f >> ${temp}
            fi
            if [ -d "./${font_and_color_themes}" ]; then
                find "./${font_and_color_themes}" -type f >> ${temp}
            fi
            if [ -d "./${key_bindings}" ]; then
                find "./${key_bindings}" -type f >> ${temp}
            fi
            echo 更新数据库...
            while read path; do
                key=`md5 -q -s "${path}"`
                value=`stat -f "%m" "${path}"`
                sqlite3 "${database}" "insert or replace into backupXcode values(NULL,'${key}',${value});" &
            done < ${temp}
        fi
        break
    fi
    if [ ${isModify} != ${value} ]; then
        if [ ${isModify} -gt ${value} ]; then
            echo "${yellow}本地Xcode配置超前${reset}!"
        else
            echo "${yellow}本地Xcode配置已经过期${reset}!"

            num=`ls -l "${cloud_backup_dir}" |grep "^-"|wc -l`
            if [ ${num} -ge 1 ]; then
                echo "${green}找到最新的Xcode配置,开始自动替换${reset}!"

                cd "${xcode_dir}"
                ## backup before
                zip -r "XcodeBackup.zip" "${code_snippets}" "${font_and_color_themes}" "${key_bindings}" &
                wait

                cd "${cloud_backup_dir}"
                newBackup=`ls -t | head -1`

                unzip -o "${newBackup}" -d "${xcode_dir}" &
                wait
            fi
        fi
        cd "${xcode_dir}"
        rm ${temp}
        if [ -d "./${code_snippets}" ]; then
            find "./${code_snippets}" -type f >> ${temp}
        fi
        if [ -d "./${font_and_color_themes}" ]; then
            find "./${font_and_color_themes}" -type f >> ${temp}
        fi
        if [ -d "./${key_bindings}" ]; then
            find "./${key_bindings}" -type f >> ${temp}
        fi
        echo 更新数据库...
        while read path; do
            key=`md5 -q -s "${path}"`
            value=`stat -f "%m" "${path}"`
            sqlite3 "${database}" "insert or replace into backupXcode values(NULL,'${key}',${value});"
        done < ${temp}
        break
    fi
done < ${temp}

wait
rm ${temp}

# zip files
cd "${xcode_dir}"
zip -r "${cloud_backup_dir}/XcodeBackup+${now}.zip" "${code_snippets}" "${font_and_color_themes}" "${key_bindings}" &
zip -r "${local_backup_dir}/XcodeBackup+${now}.zip" "${code_snippets}" "${font_and_color_themes}" "${key_bindings}" &

wait

# delete unnecessary backup files
num=`ls -l "${cloud_backup_dir}" |grep "^-"|wc -l`
if [ ${num} -gt 5 ]; then
    num=`expr ${num} - 5`
    cd "${cloud_backup_dir}"
    ls -tr "${cloud_backup_dir}" | head -${num} | xargs rm
fi

num=`ls -l "${local_backup_dir}" |grep "^-"|wc -l`
if [ ${num} -gt 5 ]; then
    num=`expr ${num} - 5`
    cd "${local_backup_dir}"
    ls -tr "${local_backup_dir}" | head -${num} | xargs rm
fi

后记

笔者简单测试了一下,基本上能用。以此思路,应该也可用在 Alfred、vimrc 等配置文件。不过依旧不是很方便,不过笔者才疏学浅,目前也就这个水平了,希望能对大家有所帮助,不知道大家有没有什么好的建议?笔者认为可以在 Xcode 关闭时自动运行本脚本,但是尚未找到好的胡克点*(:*」∠)_,如果大家有什么好的建议,欢迎PR

就这样吧,下台鞠躬!!!