macOS工作室
在 macOS工作室起步 我思考了不同的构建工作室的基础环境,例如虚拟化和容器化。我个人一直在不同的Linux环境和macOS环境中切换(有太多的设备和安装部署),每次起步初始化确实也花费了不少时间。例如我有机会能短暂使用 Apple ARM架构芯片M1 Pro ,但是很快又得切换回 Arch Linux 的 MacBook Pro 15" Late 2013 。
理想的工作方式是全面采用容器化技术( Docker )来构建,并结合 kind(本地docker模拟k8s集群) 模拟 Kubernetes ,这样在不同的工作环境中,只要采用合适的镜像( 从Dockerfile构建Docker镜像 )以及基础运行环境,就能够无缝切换。
2026年6月,我通过 使用OCLP(OpenCore Legacy Patcher)安装最新macOS 复活了我的古老的 MacBook Pro 15" Late 2013 ,并且折腾了 Colima使用NFS共享存储 来实现一个完整的准高性能 Debian镜像(tini进程管理器) 。不过,硬件还是太古早了,我想要更为清晰分工的开发环境和服务器环境,而不是全部堆积在本地笔记本上无法拆分。所以,我参考现在现代互联网大厂的标准开发工作,来实现自己个人的开发、测试、部署环境:
采用 mise 构建本地开发环境,日常开发在本地 macOS 中完成
采用自动测试工具进行代码覆盖测试
通过测试后采用 Argo - 基于Kubernetes的持续集成和工作流 自动部署到生产环境
Homebrew
要实现完整的可移植工作环境(和 Linux 对齐),在macOS环境中使用 Homebrew 来构建基础环境:
安装 homebrew :
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
安装 mise :
brew install mise
使用mise安装开发工具链:
# 1. 安装开发工具链(指定你需要的主力稳定版本)
mise use --global go@1.26.3 # 最新release
mise use --global rust@stable
mise use --global python@latest # 最新为3.14.5
mise use --global ruby@3 # 当前ruby on rails推荐3系列
gem install rails # 安装rails,当前为rails 8
mise use --global node@24 # Node.js 用于支持 Neovim 的部分前端 LSP, 安装最新LTS版本
# 2. 检查安装状态
mise ls
克隆 LazyVim Starter 骨架:
# 1. 确保配置目录干净(如果之前有旧配置,先备份)
mv ~/.config/nvim ~/.config/nvim.bak 2>/dev/null || true
# 2. 克隆官方的 LazyVim 模板骨架
git clone https://github.com/LazyVim/starter ~/.config/nvim
# 3. 移除不必要的初始化干扰,保留 .git 方便以后更新
# 在本地我们不需要 rm -rf .git,直接保留即可
为避免GUI客户端或者终端启动时没有正确加载
~/.zshrc,这里添加 neovim 入口配置,确保补全mise的二进制路径: 修改~/.config/nvim/config/options.lua:
~/.config/nvim/lua/config/options.lua 添加环境变量--- =========================================================================
-- macOS 本地特需:强行拉通 Neovim 与 mise 的环境变量总线
-- =========================================================================
-- 1. 将 mise 的 shims 和剪切路径物理追加到 Neovim 的运行态 PATH 中
-- 这样无论你在哪个目录下打开 nvim,Mason 和 LSP 都能精准找到当前目录激活的语言编译器
local mise_shims = vim.fn.expand("~/.local/share/mise/shims")
local mise_bin = vim.fn.expand("~/.local/share/mise/bin")
if vim.fn.isdirectory(mise_shims) == 1 then
vim.env.PATH = mise_shims .. ":" .. mise_bin .. ":" .. vim.env.PATH
end
-- 2. 为 Python 宿主环境提供保底的物理路径(防止 Mason 找不到 python3 报错)
-- 告诉 Neovim 直接去使用 mise 链条里的全局稳定版 python
if vim.fn.executable("python3") == 1 then
vim.g.python3_host_prog = vim.fn.exepath("python3")
end
-- 3. 同理,为 Ruby 提供保底路径
if vim.fn.executable("ruby") == 1 then
vim.g.ruby_host_prog = vim.fn.exepath("ruby")
end
首次启动nvim,
lazy.nvim插件管理器会自动拉取上百个 LazyVim 生态的核心 Lua 脚本。这里依赖前面options.lua中设置的PATH,neovim会通过~/.local/share/mise/shims识别到本地安装的go,cargo,python3
在macOS本地无需执行headless编译,可以在UI界面中静静等待它下载完成
不过,这里还是推荐如 Colima镜像 类似的静默方式命令行安装 +Lazy! sync 和 +TSUpdateSync 来完成 Lazy.nvim + Treesitter(语法高亮)安装:
+Lazy! sync 和 +TSUpdateSync# 安装Lazy.vim
nvim --headless "+Lazy! sync" +qa
# 安装Treesitter(语法高亮)
nvim --headless "+TSUpdateSync markdown python go cpp" +qa
插件下载完毕后,执行以下命令安装LSP
nvim --headless -c 'lua local r = require("mason-registry"); local list = { "clangd", "rust-analyzer", "gopls", "pyright", "ruff", "ruby-lsp" }; r.refresh(function() for _, name in ipairs(list) do local p = r.get_package(name); if not p:is_installed() then print("▶ [Mac Local] 正在静默下载: " .. name); p:install() end end end); vim.wait(300000, function() for _, name in ipairs(list) do if not r.is_installed(name) then return false end end return true end, 500); print("🎉 [Mac Local] 恭喜!所有目标多语言 LSP 已在本地全量静默固化成功!")' +qa
这里安装的LSP还需要激活:
在nvim中输入 :Mason 并回车,看到 Installed 中如果 clangd、rust-analyzer、gopls、pyright、ruff 以及 ruby-lsp 都有了绿色的 [i] 标记则表示已经完成全量热态注册
输入 :LazyExtras LazyVim 骨架,确保你开启了对应语言的 lang 扩展: 找到 lang.go、lang.rust、lang.python 等,按下 x 键激活它们。激活后,LazyVim 才会真正去调度刚刚安装好的 Mason 二进制专家。
我这里遇到过一个报错:
goimports Tried to link bin "goimports" to non-existent target "goimports".
gofumpt Tried to link bin "gofumpt" to non-existent target "gofumpt".
这个问题似乎是因为没有设置好GOPATH,并且默认的 ~/go/bin 目录不存在。因为使用了 mise ,所有的GO安装软件都在 ~/.local/share/mise/installs/go/1.26.3/bin/
而Mason需要将执行程序复制到 ~/.local/share/nvim/mason/packages/ 目录下对应模块的目录下,也就是 goimports 应该复制到 ~/.local/share/nvim/mason/packages/goimports/goimports 目录下对应模块的目录下
所以我先手工安装 goimports 和 gofumpt :
go install golang.org/x/tools/cmd/goimports@latest
go install mvdan.cc/gofumpt@latest
此时可以在 ~/.local/share/mise/installs/go/1.26.3/bin/ 目录下找到编译安装的2个执行文件,现在把它们复制到Mason对应packages目录:
# 为 goimports 创建 Mason 官方标准的物理落地目录
mkdir -p ~/.local/share/nvim/mason/packages/goimports/
# 将 mise 编译出来的真身,复制到这个专属目录下,并保持原名(这就是 Mason 软链接指向的物理目标)
cp ~/.local/share/mise/installs/go/1.26.3/bin/goimports ~/.local/share/nvim/mason/packages/goimports/
# 为 gofumpt 创建 Mason 官方标准的物理落地目录
mkdir -p ~/.local/share/nvim/mason/packages/gofumpt/
# 将 mise 编译出来的真身,同样复制到它的专属目录下
cp ~/.local/share/mise/installs/go/1.26.3/bin/gofumpt ~/.local/share/nvim/mason/packages/gofumpt/
然后在补齐配置和软连接:
# 1. 写入 goimports 配置
cat << 'EOF' > ~/.local/share/nvim/mason/packages/goimports/mason-receipt.json
{
"name": "goimports",
"schema_version": "1.1.0",
"metrics": {
"completion_time": 1700000000,
"start_time": 1700000000
},
"links": {
"bin": {
"goimports": "goimports"
},
"share": {}
},
"primary_source": {
"type": "go"
},
"secondary_sources": [],
"installed_version": "v0.10.0"
}
EOF
# 2. 写入 gofumpt 配置
cat << 'EOF' > ~/.local/share/nvim/mason/packages/gofumpt/mason-receipt.json
{
"name": "gofumpt",
"schema_version": "1.1.0",
"metrics": {
"completion_time": 1700000000,
"start_time": 1700000000
},
"links": {
"bin": {
"gofumpt": "gofumpt"
},
"share": {}
},
"primary_source": {
"type": "go"
},
"secondary_sources": [],
"installed_version": "v0.45.0"
}
EOF
# 3. 强行建立软链接(确保物理链路通畅)
ln -sf ../packages/goimports/goimports ~/.local/share/nvim/mason/bin/goimports
ln -sf ../packages/gofumpt/gofumpt ~/.local/share/nvim/mason/bin/gofumpt
此时再进入 nvim 并使用 :Mason 就会看到安装的这两个language模块成功了。
备注
这里还有一点问题是在 :Mason 面板总是提示 gomiports 和 gofumpt 需要版本升级:
new version available: - -> v0.10.0
new version available: - -> v0.45.0
但实际上已经安装好,就是不知道是哪里修复这个版本信息,看起来gemini提供的 mason-receipt.json 是正确的,但似乎还有什么隐含的配置未修复
macOS 自带的 Xcode 命令行工具里已经内置了
sourcekit-lsp只需要在 Neovim 里告诉 LazyVim 顺着系统路径去调用它即可: 创建~/.config/nvim/lua/plugins/swift.lua:
return {
-- 1. 让 Treesitter 在本地直接加载 macOS 自带的 swift 语法树解析器
{
"nvim-treesitter/nvim-treesitter",
opts = function(_, opts)
if type(opts.ensure_installed) == "table" then
vim.list_extend(opts.ensure_installed, { "swift" })
end
end,
},
-- 2. 将本地原生陆基的 sourcekit-lsp 挂载到 Neovim 的 LSP 核心管理器中
{
"neovim/nvim-lspconfig",
opts = {
servers = {
sourcekit = {
-- 物理定位 macOS 内部最高顺位的 Swift 语言服务引擎
cmd = { "/usr/bin/xcrun", "sourcekit-lsp" },
filetypes = { "swift", "objective-c", "objective-cpp" },
},
},
},
},
}
安装必要工具(可选):
brew update
# 参考 "Homebrew初始化"
# 按需安装自己需要的工具包
# neovim替换了vim
brew install pssh neovim tmux tree webp gawk gsed openconnect nginx
# 可选安装
brew install --cask keepassxc
#brew install --cask docker
#brew install --cask iterm2
#brew install golang
切换python3版本:
cd /opt/homebrew/bin
ln -s python3.11 python3
ln -s python3.11 python
ln -s pip3.11 pip3
ln -s pip3.11 pip
Python virtualenv 和 Sphinx文档
结合 Sphinx文档 同时安装和部署好 Python virtualenv Python 开发环境
虚拟沙箱环境非常简单:
cd ~
python3 -m venv venv3
激活:
# bash 使用 activate
source venv3/bin/activate
# csh 使用 activate.csh
# source venv3/bin/activate.csh
安装Sphinx 以及 rtd :
pip install sphinx
pip install sphinx_rtd_theme
pip install sphinxnotes-strike
# 支持视频、YouTube、Markdow格式和中文搜索,安装组件和配置
# 内置支持 graphviz (需要操作系统安装 graphviz 软件包)
pip install sphinxcontrib-video
pip install sphinxcontrib-youtube
pip install myst-parser
pip install jieba
Lima: Linux Machines 虚拟化
备注
我的 MacBook Pro 15" Late 2013 硬件非常古老,通过 使用OCLP(OpenCore Legacy Patcher)安装最新macOS 实现准新的 macOS 15运行已经非常消耗资源,所以再运行虚拟化和 Kubernetes 模拟虽然可以实现,但使用效率不高。
我这里仅记录我的折腾笔记,后续我实际上会采用更为轻量级的本地运行开发环境,而将重负载的Linux虚拟机全部迁移到服务器上运行。
Kubernetes 模拟
备注
另一个方案是自己构架虚拟化环境,也就是不依赖于 macOS安装Docker 获得 Hypervisor 款里框架,而是手工通过 Lima: Linux Machines 构建虚拟机。这样更好玩,更有技术挑战。
通过 Homebrew macOS安装Docker :
brew install --cask docker
brew install kind kubectl
3个管控节点,5个工作节点的集群,并且结合本地registry,采用 kind集群本地Registry 方法
kind-with-registry-macos.sh脚本:
#!/bin/sh
set -o errexit
CLUSTER_NAME='dev'
# create registry container unless it already exists
reg_name="${CLUSTER_NAME}-registry"
reg_port='5001'
if [ "$(docker inspect -f '{{.State.Running}}' "${reg_name}" 2>/dev/null || true)" != 'true' ]; then
docker run \
-d --restart=always -p "127.0.0.1:${reg_port}:5000" --name "${reg_name}" \
registry:2
fi
# create a cluster with the local registry enabled in containerd
cat <<EOF | kind create cluster --name ${CLUSTER_NAME} --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
- role: worker
- role: worker
- role: worker
- role: worker
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:${reg_port}"]
endpoint = ["http://${reg_name}:5000"]
EOF
# connect the registry to the cluster network if not already connected
if [ "$(docker inspect -f='{{json .NetworkSettings.Networks.kind}}' "${reg_name}")" = 'null' ]; then
docker network connect "kind" "${reg_name}"
fi
# Document the local registry
# https://github.com/kubernetes/enhancements/tree/master/keps/sig-cluster-lifecycle/generic/1755-communicating-a-local-registry
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
name: local-registry-hosting
namespace: kube-public
data:
localRegistryHosting.v1: |
host: "localhost:${reg_port}"
help: "https://kind.sigs.k8s.io/docs/user/local-registry/"
EOF
需要 修复kind集群重启异常 ,所以再执行:
#!/usr/bin/env bash
set -e
CLUSTER_NAME='dev'
reg_name="${CLUSTER_NAME}-registry"
# Workaround for https://github.com/kubernetes-sigs/kind/issues/2045
# all_nodes=$(kind get nodes --name "${CLUSTER_NAME}" | tr "\n" " ")
# 我将 registry 也加入了列表指定静态IP
all_nodes=$(kind get nodes --name "${CLUSTER_NAME}" | tr "\n" " ")${reg_name}
declare -A nodes_table
ip_template="{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}"
echo "Saving original IPs from nodes"
for node in ${all_nodes}; do
#nodes_table["${node}"]=$(docker inspect -f "${ip_template}" "${node}")
#registry有2个网络接口,这里采用了一个比较ugly的方法,过滤掉不属于kind网络的接口IP
nodes_table["${node}"]=$(docker inspect -f "${ip_template}" "${node}" | sed 's/172.17.0.2//')
echo "${node}: ${nodes_table["${node}"]}"
done
echo "Stopping all nodes and registry"
docker stop ${all_nodes} >/dev/null
echo "Re-creating network with user defined subnet"
subnet=$(docker network inspect -f "{{(index .IPAM.Config 0).Subnet}}" "kind")
echo "Subnet: ${subnet}"
gateway=$(docker network inspect -f "{{(index .IPAM.Config 0).Gateway}}" "kind")
echo "Gateway: ${gateway}"
docker network rm "kind" >/dev/null
docker network create --driver bridge --subnet ${subnet} --gateway ${gateway} "kind" >/dev/null
echo "Assigning static IPs to nodes"
for node in "${!nodes_table[@]}"; do
docker network connect --ip ${nodes_table["${node}"]} "kind" "${node}"
echo "Assigning IP ${nodes_table["${node}"]} to node ${node}"
done
echo "Starting all nodes and registry"
docker start ${all_nodes} >/dev/null
echo -n "Wait until all nodes are ready "
while :; do
#[[ $(kubectl get nodes | grep Ready | wc -l) -eq ${#nodes_table[@]} ]] && break
#需要启动的k8s节点比docker的容器列表少2 (registry和haproxy没有包含在k8s)
pod_num=`expr ${#nodes_table[@]} - 2`
[[ $(kubectl get nodes | grep Ready | wc -l) -eq ${pod_num} ]] && break
echo -n "."
sleep 5
done
echo