Logo

M1 芯片 Mac 搭建 ruby 2.5.7 开发环境

avatar jane 03 Sep 2024

电脑续航能力越来越差,内存、存储空间也不太够,索性换了一台二手的M1芯片电脑。作为一名 Ruby on Rails 开发者,首先要做的就是搭建开发环境,本文从直接搭建 ruby 环境和 Docker 搭建 ruby 环境两种方式来进行介绍。

目标

搭建 ruby 2.5.7 开发环境,并启动现有 Ruby on Rails 服务。

软硬件

系统:MacOS Sonoma 14.4.1
芯片:Apple M1
数据库:PostgreSQL 14
版本管理工具:asdf

背景知识

Rosetta 2 转义技术 能以一种变通的方式将为 Intel 处理器编译的程序运行于 Apple M1 芯片上。

方式一:直接搭建 ruby 环境

由于 ruby 2.6 及以下版本在安装 ARM 版时会有编译问题, 有些 ruby gem 比如 ruby-oci8 需要 x86 版 ruby 才能安装,这里就介绍如何在 ARM 架构芯片电脑上搭建 x86 版 ruby 环境。

  • 鼠标移到 iTerm 终端图标上,右键复制一个新的终端出来后,命名为 iTerm x86_64。
  • iTerm x86_64 图标右键,选择 Get info, 勾选 Open using Rosetta,重新启动终端。
  • 通过 arch -x86_64 前缀来安装 x86 版 homebrew。
# https://brew.idayer.com/guide/m1/#安装-x86-版-homebrew
$ arch -x86_64 /bin/bash -c "$(curl -fsSL <https://gitee.com/ineo6/homebrew-install/raw/master/install.sh>)"

# 设置别名,与 ARM 版 Homebrew 区分开
$ alias ibrew='arch -x86_64 /usr/local/bin/brew'
  • 安装openssl等工具:$ ibrew install openssl@1.1 readline libyaml gmp
  • 设置环境变量: export RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(ibrew --prefix openssl@1.1) --with-readline-dir=$(ibrew --prefix readline) --with-libyaml-dir=$(ibrew --prefix libyaml)"
  • 安装ruby版本: $ asdf install ruby 2.5.7

如果是安装ruby 3.1即以上版本,则需要用到openssl@3.x

此时需要重新设置环境变量:$ export RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(ibrew --prefix openssl@3) --with-readline-dir=$(ibrew --prefix readline) --with-libyaml-dir=$(ibrew --prefix libyaml)"

  • 接下来进入项目目录,执行以下命令安装 bundlergemfile.lock 中的插件:
$ gem install bundler:version # version 替换成实际版本
$ bundle install
在这个过程中可能会遇到一些问题。

1、安装pg插件报错:

Unable to find PostgreSQL client library.
Please install libpq or postgresql client package like so:
brew install libpq

那是因为 PostgreSOL 软件安装的是 ARM 架构版的,需要重新安装 x86 版的。

# 原先 ARM 版安装的 homebrew 没有使用别名,还是用 brew 命令
$ brew services stop postgresql@14(brew 对应 ARM 版本的)
$ brew uninstall postgresql@14
$ ibrew install postgresql@14(ibrew 对应 x86 版本的)
$ ibrew services start postgresql@14
# 以下命令验证 pg 插件是否能成功安装
$ gem install pg
# pg 插件可以成功安装之后,再次执行命装其余插件
$ bundle install

2、如果执行 rails console 报错:connection to server on socket "/tmp/.s.PGSQL.5432" failed: No such file or directory ,说明 PostgreSQL 没有启动成功。

执行 ibrew services info postgresql@14 查看执行情况,显示如下,果然没在运行。

postgresql@14 (homebrew.mxcl.postgresql@14)
Running: ✘
Loaded: ✔
Schedulable: ✘

查看日志 tail -f /usr/local/var/log/postgresql@14.log

发现报错信息: postgres: could not access the server configuration file "/usr/local/var/postgresql@14/postgresql.conf": No such file or directory

原来是没有初始化db, /usr/local/var/postgresql@14 目录中没有任何文件,执行 initdb -D /usr/local/var/postgresql@14 即可。

3、执行 initdb -D /usr/local/var/postgresql@14 报错:

running bootstrap script ... 2024-07-12 23:30:49.882 CST [82449] FATAL:  could not create shared memory segment: Cannot allocate memory
2024-07-12 23:30:49.882 CST [82449] DETAIL:  Failed system call was shmget(key=8862685, size=56, 03600).
2024-07-12 23:30:49.882 CST [82449] HINT:  This error usually means that PostgreSQL's request for a shared memory segment exceeded your kernel's SHMALL parameter.  You might need to reconfigure the kernel with larger SHMALL.

这通常是因为 PostgreSQL 请求的共享内存段超出了系统的 SHMALL 参数。

# 执行命令:
sysctl kern.sysv.shmmax
sysctl kern.sysv.shmmin
sysctl kern.sysv.shmmni
sysctl kern.sysv.shmseg
sysctl kern.sysv.shmall

# 可以看到返回以下内容:
kern.sysv.shmmax: 4194304
kern.sysv.shmmin: 1
kern.sysv.shmmni: 32
kern.sysv.shmseg: 8
kern.sysv.shmall: 1024

在 macOS 上,可以通过修改 /etc/sysctl.conf 文件来调整共享内存设置。如果文件不存在,可以创建它: sudo vi /etc/sysctl.conf,然后添加以下内容:

kern.sysv.shmmax: 4194304
kern.sysv.shmmin: 1
kern.sysv.shmmni: 32
kern.sysv.shmseg: 8
kern.sysv.shmall: 65536

改了kern.sysv.shmall, 1024 -> 65536

保存修改后重启系统,新的配置才会生效,可以重新执行 initdb,并重启 PostgreSQL 了。

4、正常到这一步就可以启动 rails console了,但是有些项目还会遇到如下报错:

connection to server on socket "/tmp/.s.PGSQL.5432" failed: could not create socket: Too many open files (ActiveRecord::ConnectionNotEstablished)

一开始以为是句柄数不够,通过 ulimit -n 句柄数 增加数量,但是没有用。

后来执行了 rails dbconsole,错误提示发生了变化:psql: error: connection to server on socket "/tmp/.s.PGSQL.5432" failed: FATAL: database "tc_mini_site_development" does not exist
原来是因为没有创建数据库…
执行 rails db:setup 创建并初始化数据库就好了。

方式二:Docker 搭建 ruby 环境

安装图形界面版 Docker: brew install docker --cask

项目根目录添加文件 Dockerfiledocker-entrypoint.shsidekiq-entrypoint.shdocker-compose.yml.env

Dockerfile 文件:

# 指定基础镜像为ruby:2.5.7,所有操作将基于此镜像。
FROM ruby:2.5.7
# 设置环境变量LANG为C.UTF-8,用于支持UTF-8编码。
ENV LANG=C.UTF-8
# 设置环境变量BUNDLER_VERSION为2.0.2,用于指定Bundler的版本。
ENV BUNDLER_VERSION=2.0.2
# 更新APT包索引,并升级所有已安装的包到最新版本。
RUN apt-get update && apt-get upgrade -y
# 使用APT安装构建和开发Ruby应用所需的各种依赖库以及工具,并避免安装不必要的推荐包。
RUN apt install -y build-essential libssl-dev libpq-dev libxml2-dev libxslt1-dev \\
        git imagemagick libbz2-dev libjpeg-dev libevent-dev libmagickcore-dev \\
        libffi-dev libglib2.0-dev zlib1g-dev libyaml-dev vim --no-install-recommends
# 安装Node.js和Yarn包管理器,用于管理前端依赖。
RUN apt install -y nodejs yarn
# 安装指定版本的Bundler。
RUN gem install bundler -v 2.0.2
# 设置工作目录为/inf-idea-backend,后续命令将在该目录下执行。
WORKDIR /inf-idea-backend
# 将当前目录下的Gemfile和Gemfile.lock文件复制到容器的工作目录中。
COPY Gemfile Gemfile.lock ./
# 运行Bundler安装Gemfile中的所有依赖。
RUN bundle install
# 将当前目录下的所有文件复制到容器的工作目录中。
COPY . ./

# Add a script to be executed every time the container starts.
# 指定容器启动时执行的入口点脚本。
ENTRYPOINT ["./docker-entrypoint.sh"]

docker-entrypoint.sh 文件:

#!/bin/bash
# 指定脚本使用的解释器为bash。

# 当命令执行出错时,立即退出脚本。
set -e

# 检查tmp/pids/server.pid文件是否存在,如果存在则删除它,以防止Rails服务器在启动时因PID冲突而无法正常启动。
if [ -f tmp/pids/server.pid ]; then
  rm tmp/pids/server.pid
fi

# 执行数据库迁移,将数据库结构更新为最新版本。
bundle exec rails db:migrate
# 启动Rails服务器,并将其绑定到所有可用的网络接口(0.0.0.0)。
bundle exec rails s -b 0.0.0.0

sidekiq-entrypoint.sh 文件:

#!/bin/sh

set -e
 
bundle exec sidekiq

docker-compose.yml 文件

services:
  docker_app: 
    tty: true # 为容器分配一个伪终端,便于调试。
    stdin_open: true # 保持标准输入打开,以便可以在容器中进行交互操作。
    build:
      context: . # 构建镜像时,使用当前目录作为上下文。
      dockerfile: Dockerfile # 使用当前目录中的Dockerfile文件构建镜像(默认行为)。
    depends_on: # 指定依赖的服务,确保在启动`docker_app`服务之前,`docker_database`和`docker_redis`服务已启动。
      - docker_database
      - docker_redis
    ports: 
      - "3000:3000" # 将容器的3000端口映射到主机的3000端口。
    volumes:
      - .:/inf-idea-backend # 将当前目录挂载到容器内的`/inf-idea-backend`路径下。
      - gem_cache:/usr/local/bundle/gems # 将名为`gem_cache`的卷挂载到容器内的`/usr/local/bundle/gems`路径下,用于缓存Gem依赖。
      - node_modules:/inf-idea-backend/node_modules # 将名为`node_modules`的卷挂载到容器内的`/inf-idea-backend/node_modules`路径下,用于缓存Node.js依赖。
    env_file: .env # 加载`.env`文件中的环境变量。
    environment:
      RAILS_ENV: development # 设置环境变量`RAILS_ENV`为`development`,表示使用开发环境。

  docker_database:
    image: postgres:14.11 # 使用`postgres:14.11`镜像作为数据库服务。
    environment:
      - "POSTGRES_HOST_AUTH_METHOD=trust" # 设置环境变量,允许Postgres使用信任认证方式,无需密码登录。
    volumes:
      - db_data:/var/lib/postgresql/data # 将名为`db_data`的卷挂载到容器内的`/var/lib/postgresql/data`路径下,用于持久化数据库数据。
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql # 将当前目录中的`init.sql`文件挂载到容器内的`/docker-entrypoint-initdb.d/init.sql`路径下,用于初始化数据库。

  docker_redis:
    image: redis:7.2.4  # 使用`redis:7.2.4`镜像作为Redis服务。

  docker_sidekiq:
    build:
      context: . # 构建镜像时,使用当前目录作为上下文。
      dockerfile: Dockerfile # 使用当前目录中的Dockerfile文件构建镜像。
    depends_on: # 指定依赖的服务,确保在启动`docker_sidekiq`服务之前,`docker_app`、`docker_database`和`docker_redis`服务已启动。
      - docker_app      
      - docker_database
      - docker_redis
    volumes:
      - .:/inf-idea-backend # 将当前目录挂载到容器内的`/inf-idea-backend`路径下。
      - gem_cache:/usr/local/bundle/gems # 将名为`gem_cache`的卷挂载到容器内的`/usr/local/bundle/gems`路径下,用于缓存Gem依赖。
      - node_modules:/app/node_modules # 将名为`node_modules`的卷挂载到容器内的`/app/node_modules`路径下,用于缓存Node.js依赖。
    env_file: .env
    environment:
      RAILS_ENV: developmen
    entrypoint: ./sidekiq-entrypoint.sh # 指定容器启动时执行的入口点脚本为`./sidekiq-entrypoint.sh`。

volumes:
  gem_cache: # 定义一个名为`gem_cache`的卷,用于缓存Gem依赖。
  db_data: # 定义一个名为`db_data`的卷,用于持久化数据库数据。
  node_modules: # 定义一个名为`node_modules`的卷,用于缓存Node.js依赖。

.env 文件添加环境变量配置 Docker 容器中的 Redis 和 Postgres 服务:

DATABASE_NAME=
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=
DATABASE_HOST=docker_database

REDIS_SERVER=docker_redis
REDIS_PORT=6379
REDIS_DB_NUM=0

database.yml 文件使用环境变量配置数据库:

<% database = Rails.application.credentials.dig(Rails.env, :database) %>
...
development:
  <<: *default
  database: <%= ENV["DATABASE_NAME"] || database.dig(:name) %>
  username: <%= ENV["DATABASE_USERNAME"] || database.dig(:username) %>
  password: <%= ENV["DATABASE_PASSWORD"] || database.dig(:password) %>
  host: <%= ENV["DATABASE_HOST"] || '127.0.0.1' %>
  username: <%= ENV["DATABASE_USERNAME"] || 'postgres' %>
...

Redis 服务配置也一样需要换成上面定义的环境变量,这里就不赘述。

使用命令docker compose up --build同时启动四个服务。

如果只想启动一个服务,可以在 up 后面加上服务名称,比如docker compose up -d docker_app --build , 其中 -d 选项表示后台运行。也可以直接在 Docker 应用界面上点击启动按钮。

--build 参数表示构建环境,第一次启动时或者有添加新的 ruby gem 时,需要添加该参数,其他时候可省略。

执行 docker compose exec docker_app bash 进入容易 shell 交互环境,此时可执行 rails db:migraterails console 等命令。

总结

一开始使用 Docker 搭建方式启动项目,用了一段时间后,发现用起来不是很方便,后来还是使用了直接安装ruby搭建环境的方式。

使用 Docker 容器的话,shell 环境用的就不是用本地的了:

1、用不了本地终端安装的便捷工具,比如 Oh My Zsh,还有 zzsh-autosuggestionsrails 等插件。本地可以配置并通过简洁命令 rc 移动 rails console,而 docker 中就是原生的bash终端环境,如果想要使用相同的便捷操作,只能再配置一遍。

2、为了能在 docker compose exec docker_app bash,即 Docker 容器 shell 中执行 cap staging rails console 以连接远端测试服务器,需要把本地的 id_rsa、id_rsa.pub 和 beansmile.ssh (自主配置连接信息文件) 放到项目目录中,目录结构如下

然后在 Dockerfile 文件中添加以下脚本,将内容复制到 docker WORKDIR 相应文件夹中才行。

# 设置SSH相关配置。
# 在用户主目录下创建.ssh目录。
RUN mkdir ~/.ssh
# 将私钥id_rsa复制到.ssh目录中。
RUN cp ssh/id_rsa ~/.ssh/
# 将公钥id_rsa.pub复制到.ssh目录中。
RUN cp ssh/id_rsa.pub ~/.ssh/
# 将当前目录的ssh_config目录下的所有文件复制到.ssh/ssh_config/目录中。
RUN cp -R ssh/ssh_config/ ~/.ssh/ssh_config/
# 创建一个空的SSH配置文件。
RUN touch ~/.ssh/config
# 向SSH配置文件中写入配置,以包含特定的SSH设置。
RUN echo 'Include ./ssh_config/beansmile.ssh' > ~/.ssh/config

3、 Docker 容器 shell 中只能使用 vim 编辑器打开 credentials 文件,而本地可以使用 sublime 等有图形界面的编辑器打开。

参考文档

苹果 Apple M1 芯片:Rosetta 2 转译技术

Containerizing a Ruby on Rails Application for Development with Docker Compose

Tags
ruby
docker
M1芯片