macOS工作室

macOS工作室起步 我思考了不同的构建工作室的基础环境,例如虚拟化和容器化。我个人一直在不同的Linux环境和macOS环境中切换(有太多的设备和安装部署),每次起步初始化确实也花费了不少时间。例如我有机会能短暂使用 Apple ARM架构芯片M1 Pro ,但是很快又得切换回 Arch LinuxMacBook 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进程管理器) 。不过,硬件还是太古早了,我想要更为清晰分工的开发环境和服务器环境,而不是全部堆积在本地笔记本上无法拆分。所以,我参考现在现代互联网大厂的标准开发工作,来实现自己个人的开发、测试、部署环境:

Homebrew

要实现完整的可移植工作环境(和 Linux 对齐),在macOS环境中使用 Homebrew 来构建基础环境:

  • 安装 homebrew :

通过网络安装Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
安装mise
brew install mise
  • 使用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 骨架:

克隆 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

通过Mason安装本地需要的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 目录下对应模块的目录下

所以我先手工安装 goimportsgofumpt :

手工安装 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目录:

按照Mason的packages目录复制 goimports 和 gofumpt
# 为 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 面板总是提示 gomiportsgofumpt 需要版本升级:

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 :

创建 ~/.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" },
        },
      },
    },
  },
}
  • 安装必要工具(可选):

在macOS新系统必装的brew软件
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版本:

切换macOS的python3版本到homebrew提供的版本
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 virtualenvSphinx文档

虚拟沙箱环境非常简单:

venv初始化
cd ~
python3 -m venv venv3
  • 激活:

激活venv
# bash 使用 activate
source venv3/bin/activate

# csh 使用 activate.csh
# source venv3/bin/activate.csh
  • 安装Sphinx 以及 rtd :

通过virtualenv的Python环境安装sphinx doc
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安装Docker Desktop for macOS
brew install --cask docker
在macOS平台上安装kind
brew install kind kubectl
  • 3个管控节点,5个工作节点的集群,并且结合本地registry,采用 kind集群本地Registry 方法 kind-with-registry-macos.sh 脚本:

运行Registry适配kind集群(dev),macOS环境的Docker Desktop for macOS
#!/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_static_ips.sh 脚本设置kind集群每个node静态IP
#!/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