A:Mac依赖与运行

This commit is contained in:
jimmy committed 2025-08-23 00:43:52 +08:00
1 parent 0e0b8bfd81
commit 96049909a3
10 files changed
+450 -11

No files matched your search

+65
View File
@@ -37,6 +37,11 @@
## 快速开始
> 目前项目处于开发阶段,依赖并不稳定,所以对于 imagemagick 与 libips 在 mac 端均需要使用
> homebrew 进行手动安装,其中 exiftool 的静态依赖以提供。
>
> 未来版本会根据对应的依赖来完成静态依赖以及动态依赖的配置和安装。
### 1. 克隆项目
```bash
@@ -54,6 +59,66 @@ go mod download
项目使用自动工具链管理系统。在开发环境下,程序会自动从 `tools/` 目录解压所需的工具:
开发环境依赖安装(macOS)
本项目需要依赖 Homebrew、ImageMagick 和 libvips。以下步骤介绍如何在 macOS 上安装这些工具。
1. 安装 Homebrew
Homebrew 是 macOS 的包管理工具,可以方便地安装和管理各种依赖。
在终端中执行以下命令安装 Homebrew:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
安装完成后,执行以下命令确认 Homebrew 是否安装成功:
brew --version
输出类似以下结果,表示安装成功:
Homebrew 4.x.x
2. 安装 ImageMagick
安装 Homebrew 之后,可以使用它安装 ImageMagick:
brew install imagemagick
验证安装是否成功:
magick -version
如果看到类似以下输出,表示安装成功:
Version: ImageMagick 7.x.x
3. 安装 libvips
同样使用 Homebrew 安装 libvips:
brew install vips
验证安装是否成功:
vips --version
输出类似以下结果,表示安装成功:
vips-8.x.x
4. 注意事项
如果在安装过程中遇到网络问题(例如下载依赖失败),可以配置代理
避免同时使用多个 homebrew 会出现锁竞争
#### 目录结构说明
```
tools/
+1 -1
View File
@@ -2,7 +2,7 @@
# 只包含需要自定义的配置项,其他将使用默认值
app:
port: "9483"
port: "9484"
mode: "release"
# 开发阶段标识 - 设置为 false 或删除此行启用生产模式
development: true
+281
View File
@@ -0,0 +1,281 @@
# macOS 签名与 ImageMagick 开发完整指南
------
## 1. macOS 签名简介
### 1.1 什么是 macOS 签名(Code Signing)
macOS 签名是苹果为保护系统安全提供的一种机制,核心目的:
- **验证应用/可执行文件的来源**
- **保证文件未被篡改**
- 与 **Gatekeeper** 联动:阻止未经验证的软件运行
签名信息嵌入二进制文件或 bundle 内,通过 **证书 Authority** 确定身份。
------
### 1.2 为什么需要签名
1. **防止 Gatekeeper 弹窗**
- 未签名或自签名文件在 macOS 上运行时,会弹窗提示“无法验证软件是否包含恶意内容”
2. **保证可分发性**
- 只有官方 Developer ID + notarization 才能开箱即用,无需用户手动允许
3. **确保动态库调用安全**
- 对于依赖第三方 dylib 的程序(如 ImageMagick),签名可防止系统拒绝加载
------
## 2. macOS 签名机制
macOS 支持多种签名类型:
| 类型 | 描述 | 适用场景 |
| -------------------------- | ------------------------------- | ---------------------- |
| 自签名(Self-signed) | 自己生成的证书,无苹果官方背书 | 开发阶段测试、内部使用 |
| 官方 Developer ID | 苹果开发者证书,可 notarization | 正式分发,用户无需弹窗 |
| 临时或自签名 Keychain 证书 | 仅在本机信任 | 适合多开发者内部测试 |
> 注意:自签名证书在其他 Mac 上仍会被 Gatekeeper 阻止,需要手动允许或移除 quarantine 属性。
------
## 3. 创建自签名证书(开发阶段)
### 3.1 准备工具
- macOS 自带 OpenSSL
- Keychain Access(钥匙串访问)
### 3.2 生成私钥和证书
```
# 生成私钥
openssl genrsa -out mykey.pem 2048
# 生成自签名证书
openssl req -new -x509 -key mykey.pem -out mycert.pem -days 3650
```
- `mykey.pem`:私钥
- `mycert.pem`:证书
- 生成时会提示填写一些信息,可随意填写开发相关内容
------
### 3.3 导入 Keychain
1. 双击 `mycert.pem`
2. 导入 Keychain 时,选择 **登录/系统/本机**(开发测试推荐选择“登录”)
3. 打开 Keychain Access → 找到证书 → 设置 **始终信任**
------
### 3.4 验证签名是否存在
```
security find-identity -p codesigning -v
```
- 如果看到类似:
```
1) 85BBDE294CE5D1F4B8E939F272B30747DA5C86B0 "duyl328"
1 valid identities found
```
说明自签名证书已创建成功,可用于签名。
------
## 4. 使用 codesign 对文件签名
### 4.1 签名可执行文件
```
codesign --force --sign "duyl328" /path/to/executable
```
说明:
- `--force`:覆盖已有签名
- `--sign`:指定证书名称或 SHA-1
- `--deep`:可递归签名 bundle 内所有二进制(适合 .app)
### 4.2 验证签名
```
codesign -dvv /path/to/executable
```
输出示例:
```
Executable=/path/to/executable
Identifier=magick
Format=Mach-O thin (x86_64)
CodeDirectory v=20400 size=447 flags=0x0(none) hashes=9+2 location=embedded
Signature size=1728
Authority=duyl328
```
- `Authority=duyl328` → 签名生效
- `TeamIdentifier` 自签名通常为空
### 4.3 注意事项
- 自签名文件在 **其他 Mac 上仍会弹窗**
- 命令行执行可通过手动允许或移除 quarantine:
```
xattr -r -d com.apple.quarantine /path/to/executable
```
------
## 5. ImageMagick 开发与签名实践
### 5.1 问题回顾
- 下载官方 tar.gz 后,解压 `/Users/xxx/ImageMagick-7.0.10`
- 执行:
```
/Users/xxx/ImageMagick-7.0.10/bin/magick -version
```
出现弹窗 → Gatekeeper 提示
执行 dyld 找不到 libMagickCore 或 freetype dylib → 动态库路径问题
------
### 5.2 解决步骤
#### 5.2.1 开发阶段
1. **签名 magick 和 lib/\*.dylib**
```
codesign --force --sign "duyl328" /Users/xxx/ImageMagick-7.0.10/bin/magick
codesign --force --sign "duyl328" /Users/xxx/ImageMagick-7.0.10/lib/*.dylib
```
1. **DYLD 临时设置**
```
DYLD_LIBRARY_PATH=/Users/xxx/ImageMagick-7.0.10/lib \
/Users/xxx/ImageMagick-7.0.10/bin/magick -version
```
- 不污染全局环境
- 解决可执行文件找不到 dylib 问题
1. **安装第三方依赖(如 freetype)**
```
brew install freetype
brew install libpng jpeg libtiff
```
- 或安装 XQuartz(自带 freetype 库)
------
#### 5.2.2 打包阶段
1. 将 magick 和 lib/*.dylib 放入 `.app/Contents/MacOS/` 和 `.app/Contents/Frameworks/`
2. 修正 dylib 路径:
```
install_name_tool -change /原始路径/libMagickCore-7.Q16HDRI.8.dylib \
@rpath/libMagickCore-7.Q16HDRI.8.dylib \
/path/to/magick
```
1. **签名整个 bundle**
```
codesign --deep --force --sign "Developer ID Application: YourName" MyApp.app
```
1. **Notarization(苹果服务器验证)**
```
xcrun altool --notarize-app -f MyApp.zip --primary-bundle-id com.your.appid -u your@appleid.com -p app-specific-password
```
- 通过 notarization 后,用户开箱即可运行,无弹窗
------
## 6. 开发阶段实用技巧
- **自签名只在本机有效**
- **DYLD_LIBRARY_PATH** 可以在单条命令中临时指定,不污染全局环境
- **xattr -d com.apple.quarantine** 可移除下载文件的隔离标记
- Homebrew 安装 ImageMagick + 依赖是最简单的开发方式
- 打包分发时需处理动态库路径、签名和 notarization
------
## 7. 总结流程
| 阶段 | 步骤 |
| -------- | ------------------------------------------------------------ |
| 开发测试 | 1. 自签名证书 → 2. codesign 可执行文件 + dylib → 3. 临时 DYLD_LIBRARY_PATH → 4. 安装必要依赖(Homebrew/XQuartz) |
| 分发 | 1. 打包可执行文件 + dylib 到 .app bundle → 2. install_name_tool 调整路径 → 3. codesign --deep 使用官方 Developer ID → 4. Notarization → 5. 生成 dmg 或 zip 分发 |
------
## 8. FAQ
**Q1:自签名文件在其他 Mac 上可用吗?**
A1:只能运行,但会出现 Gatekeeper 弹窗,需要手动允许或解除 quarantine。
**Q2:为什么 DYLD_LIBRARY_PATH 不全局?**
A2:开发阶段临时指定即可,不污染用户全局环境,方便测试。
**Q3:为什么 Homebrew 安装的 Magick 可以直接用?**
A3:Homebrew 自动处理了 dylib 路径和依赖库,符合 macOS rpath 机制。
**Q4:正式发布必须买开发者账号吗?**
A4:是的,要开箱即用、无弹窗,必须使用 **Developer ID + notarization**。
------
## 9. 参考命令汇总
```
# 生成自签名证书
openssl genrsa -out mykey.pem 2048
openssl req -new -x509 -key mykey.pem -out mycert.pem -days 3650
# 导入钥匙串
open mycert.pem
# 查看签名身份
security find-identity -p codesigning -v
# 签名可执行文件
codesign --force --sign "duyl328" /path/to/executable
codesign --force --sign "duyl328" /path/to/lib/*.dylib
# 验证签名
codesign -dvv /path/to/executable
# 移除 quarantine 属性
xattr -r -d com.apple.quarantine /path/to/executable
# 临时执行 ImageMagick,指定 dylib 路径
DYLD_LIBRARY_PATH=/path/to/lib /path/to/bin/magick -version
# 修改 dylib 依赖路径
install_name_tool -change /旧路径/libX.dylib @rpath/libX.dylib /path/to/magick
# Homebrew 安装 ImageMagick
brew install imagemagick
magick -version
```
------
@@ -2,9 +2,10 @@ package repositories
import (
"context"
"gorm.io/gorm"
"rear/internal/db"
"rear/internal/model/tables"
"gorm.io/gorm"
)
// ExifRepository EXIF数据仓库
+81 -6
View File
@@ -5,7 +5,6 @@ import (
"context"
"encoding/json"
"fmt"
"go.uber.org/zap"
"os"
"os/exec"
"path/filepath"
@@ -15,6 +14,8 @@ import (
"strings"
"sync"
"time"
"go.uber.org/zap"
)
// 全局变量存储工具路径
@@ -34,7 +35,7 @@ type Config struct {
VipsPath string
}
// Initialize 使用配置文件初始化工具路径
// InitializeFromConfig Initialize 使用配置文件初始化工具路径
func InitializeFromConfig(baseDir *string) error {
toolsInitOnce.Do(func() {
// 从全局配置获取工具路径
@@ -96,7 +97,7 @@ func detectTools(baseDir *string) error {
}
}
if ImageMagickPath == "" {
return fmt.Errorf("ImageMagick not found")
logger.Warn("ImageMagick not found")
}
// 检测 ExifTool
@@ -118,7 +119,7 @@ func detectTools(baseDir *string) error {
}
}
if ExifToolPath == "" {
return fmt.Errorf("ExifTool not found")
logger.Warn("ExifTool not found")
}
// 检测 libvips
@@ -132,7 +133,7 @@ func detectTools(baseDir *string) error {
}
}
if VipsPath == "" {
return fmt.Errorf("libvips not found")
logger.Warn("libvips not found")
}
logger.Info("工具路径检测完成",
@@ -198,13 +199,29 @@ func findTool(name string, execDir string, identification string) string {
filepath.Join(execDir, "tools", identification, name, "bin", exeName),
filepath.Join(execDir, "tools", identification, "bin", name, exeName),
filepath.Join(execDir, "tools", identification, "bin", exeName),
// 额外路径:针对解压后的工具结构 (如 tools/exiftool/exiftool)
filepath.Join(execDir, "tools", name, name),
filepath.Join(execDir, "tools", identification, identification),
}
jsonBytes, _ := json.Marshal(searchPaths)
msg := string(jsonBytes)
logger.Debug("查找工具路径", zap.String("tool", name), zap.String("paths", msg))
for _, path := range searchPaths {
if _, err := os.Stat(path); err == nil {
if info, err := os.Stat(path); err == nil {
// 确保找到的是可执行文件而不是目录
if info.IsDir() {
logger.Debug("跳过目录", zap.String("tool", name), zap.String("path", path))
continue
}
// 在 macOS 和 Linux 上确保工具有执行权限
if runtime.GOOS != "windows" {
if err := ensureExecutable(path); err != nil {
logger.Warn("设置工具执行权限失败", zap.String("tool", name), zap.String("path", path), zap.Error(err))
// 即使权限设置失败,仍然继续尝试使用该工具
}
}
logger.Info("找到工具", zap.String("tool", name), zap.String("path", path))
return path
}
@@ -212,6 +229,12 @@ func findTool(name string, execDir string, identification string) string {
// 尝试从 PATH 中查找
if path, err := exec.LookPath(exeName); err == nil {
// 在 macOS 和 Linux 上确保工具有执行权限
if runtime.GOOS != "windows" {
if err := ensureExecutable(path); err != nil {
logger.Warn("设置系统工具执行权限失败", zap.String("tool", name), zap.String("path", path), zap.Error(err))
}
}
logger.Info("从系统PATH找到工具", zap.String("tool", name), zap.String("path", path))
return path
}
@@ -414,6 +437,34 @@ func getVipsVersion() (string, error) {
return output, nil
}
// ensureExecutable 确保文件具有执行权限(仅适用于 Unix 系统)
func ensureExecutable(path string) error {
if runtime.GOOS == "windows" {
return nil // Windows 不需要设置执行权限
}
// 获取当前文件权限
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("无法获取文件信息: %w", err)
}
// 检查是否已有执行权限
mode := info.Mode()
if mode&0111 != 0 {
return nil // 已有执行权限
}
// 添加执行权限 (相当于 chmod +x)
newMode := mode | 0111
if err := os.Chmod(path, newMode); err != nil {
return fmt.Errorf("设置执行权限失败: %w", err)
}
logger.Debug("已设置工具执行权限", zap.String("path", path), zap.String("mode", newMode.String()))
return nil
}
// CommandResult 命令执行结果
type CommandResult struct {
Stdout []byte
@@ -424,6 +475,10 @@ type CommandResult struct {
// ExecuteCommand 执行命令的通用函数
func ExecuteCommand(ctx context.Context, program string, args ...string) (*CommandResult, error) {
// 记录命令执行信息
cmdStr := fmt.Sprintf("%s %s", program, strings.Join(args, " "))
logger.Debug("执行命令", zap.String("command", cmdStr))
// 创建命令
cmd := exec.CommandContext(ctx, program, args...)
@@ -454,5 +509,25 @@ func ExecuteCommand(ctx context.Context, program string, args ...string) (*Comma
result.ExitCode = -1
}
// 详细的错误日志记录
if err != nil {
stdoutStr := string(result.Stdout)
stderrStr := string(result.Stderr)
logger.Error("命令执行失败",
zap.String("command", cmdStr),
zap.String("program", program),
zap.Strings("args", args),
zap.Int("exitCode", result.ExitCode),
zap.Duration("duration", duration),
zap.String("stdout", stdoutStr),
zap.String("stderr", stderrStr),
zap.Error(err))
} else {
logger.Debug("命令执行成功",
zap.String("command", cmdStr),
zap.Int("exitCode", result.ExitCode),
zap.Duration("duration", duration))
}
return result, err
}
+16
View File
@@ -66,3 +66,19 @@
{"level":"info","time":"2025-08-21 21:20:29.322","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-21 21:23:55.718","caller":"config/config.go:90","msg":"加载配置文件成功","path":"D:\\go-argus\\src\\rear\\config.yaml"}
{"level":"info","time":"2025-08-22 13:37:42.034","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 18:35:04.796","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:04:07.099","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:05:33.667","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:08:17.494","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:09:03.404","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:09:24.974","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:09:39.638","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:12:51.093","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:16:00.129","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:16:39.685","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:19:38.902","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:32:47.109","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:40:12.897","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 19:41:04.261","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-22 23:48:45.047","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
{"level":"info","time":"2025-08-23 00:17:09.951","caller":"config/config.go:90","msg":"加载配置文件成功","path":"config.yaml"}
+2 -1
View File
@@ -5,7 +5,6 @@ import (
"context"
"errors"
"fmt"
"go.uber.org/zap"
"io"
"log"
"net/http"
@@ -26,6 +25,8 @@ import (
"syscall"
"time"
"go.uber.org/zap"
"github.com/gin-contrib/cors"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
+1 -1
View File
@@ -1,6 +1,6 @@
### Argus 照片管理系统 - 照片接口测试
### 服务器地址
@baseUrl = http://localhost:8080
@baseUrl = http://localhost:9484¡
@apiBase = {{baseUrl}}/api/v1
### 注意:需要先运行图像库索引获得实际的hash值
+1 -1
View File
@@ -1,6 +1,6 @@
### 时间线和照片列表接口测试
@baseUrl = http://localhost:9483
@baseUrl = http://localhost:9484
@api = {{baseUrl}}/api/v1
### 1. 获取照片时间线统计(全部数据)