Skip to content

Commit a7db4ac

Browse files
committed
fix:修复Android 平台上,exec.Command 无法执行从应用缓存目录提取的二进制文件的问题
1 parent e4df4b7 commit a7db4ac

File tree

5 files changed

+334
-11
lines changed

5 files changed

+334
-11
lines changed

.github/workflows/build.yml

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,9 @@ jobs:
9292
- name: Verify dependencies
9393
run: go mod verify
9494

95-
- name: Download ECS binaries for embed
95+
- name: Download ECS binaries and prepare for Android
9696
run: |
9797
REPO="oneclickvirt/ecs"
98-
BINARIES_DIR="embedding/binaries"
99-
100-
mkdir -p "$BINARIES_DIR"
10198
10299
# 使用 gh CLI 获取最新版本
103100
echo "获取最新版本信息..."
@@ -110,24 +107,30 @@ jobs:
110107
111108
echo "ECS 版本: $ECS_VERSION"
112109
110+
# 创建 jniLibs 目录
111+
mkdir -p jniLibs/arm64-v8a
112+
mkdir -p jniLibs/x86_64
113+
113114
# 下载 Linux ARM64(用于 Android ARM64)
114115
echo "下载 Linux ARM64..."
115116
gh release download "$ECS_VERSION" --repo "$REPO" --pattern "goecs_linux_arm64.zip" --output "/tmp/goecs_linux_arm64.zip"
116117
unzip -q -o "/tmp/goecs_linux_arm64.zip" -d /tmp/
117-
mv /tmp/goecs "${BINARIES_DIR}/goecs-linux-arm64"
118-
chmod +x "${BINARIES_DIR}/goecs-linux-arm64"
118+
mv /tmp/goecs jniLibs/arm64-v8a/libgoecs.so
119+
chmod 755 jniLibs/arm64-v8a/libgoecs.so
119120
120121
# 下载 Linux AMD64(用于 Android x86_64)
121122
echo "下载 Linux AMD64..."
122123
gh release download "$ECS_VERSION" --repo "$REPO" --pattern "goecs_linux_amd64.zip" --output "/tmp/goecs_linux_amd64.zip"
123124
unzip -q -o "/tmp/goecs_linux_amd64.zip" -d /tmp/
124-
mv /tmp/goecs "${BINARIES_DIR}/goecs-linux-amd64"
125-
chmod +x "${BINARIES_DIR}/goecs-linux-amd64"
125+
mv /tmp/goecs jniLibs/x86_64/libgoecs.so
126+
chmod 755 jniLibs/x86_64/libgoecs.so
126127
127128
echo ""
128-
echo "二进制文件列表:"
129-
ls -lh "${BINARIES_DIR}/"
129+
echo "jniLibs 文件列表:"
130+
ls -lh jniLibs/*/libgoecs.so
130131
echo ""
132+
echo "文件大小:"
133+
du -sh jniLibs/*/libgoecs.so
131134
env:
132135
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
133136

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,19 @@ go install fyne.io/fyne/v2/cmd/fyne@latest
4545
### 构建命令
4646

4747
```bash
48+
# Android 构建前准备:需要先准备 ECS 二进制文件
49+
# 从 ECS 项目编译 Linux 二进制并放入 jniLibs 目录
50+
# 详见 jniLibs/README.md
51+
52+
# 快速准备命令(假设 ecs 项目在 ../ecs)
53+
cd ../ecs && \
54+
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -checklinkname=0" -o goecs-linux-arm64 ./ && \
55+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -checklinkname=0" -o goecs-linux-amd64 ./ && \
56+
cd ../goecs && \
57+
cp ../ecs/goecs-linux-arm64 jniLibs/arm64-v8a/libgoecs.so && \
58+
cp ../ecs/goecs-linux-amd64 jniLibs/x86_64/libgoecs.so && \
59+
chmod 755 jniLibs/*/libgoecs.so
60+
4861
# 构建桌面端(用于快速测试)
4962
./build.sh desktop
5063

embedding/embed_android.go

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
//go:build android
2+
3+
package embedding
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"runtime"
10+
"strings"
11+
)
12+
13+
// findNativeLibraryDir 查找应用的 native library 目录
14+
// 这个目录是系统自动管理的,包含从 APK 中提取的 .so 文件
15+
func findNativeLibraryDir() (string, error) {
16+
// 方法 1: 通过可执行文件路径推断
17+
execPath, err := os.Executable()
18+
if err == nil {
19+
// 可执行文件通常在 /data/app/<package>-<hash>/base.apk 或 /data/app/<package>-<hash>/oat/arm64/base.odex
20+
// native library 通常在 /data/app/<package>-<hash>/lib/arm64/
21+
22+
// 尝试找到应用根目录
23+
dir := execPath
24+
for i := 0; i < 5; i++ { // 最多向上查找5层
25+
dir = filepath.Dir(dir)
26+
libDir := filepath.Join(dir, "lib")
27+
28+
// 检查 lib 目录
29+
if info, err := os.Stat(libDir); err == nil && info.IsDir() {
30+
// 检查是否包含架构子目录
31+
entries, err := os.ReadDir(libDir)
32+
if err == nil && len(entries) > 0 {
33+
// 如果 lib 目录有子目录(架构名),返回 lib 目录
34+
for _, entry := range entries {
35+
if entry.IsDir() {
36+
return libDir, nil
37+
}
38+
}
39+
// 如果 lib 目录直接包含 .so 文件,也返回
40+
return libDir, nil
41+
}
42+
}
43+
}
44+
}
45+
46+
// 方法 2: 尝试标准的 Android native library 路径
47+
possibleBasePaths := []string{
48+
"/data/data/com.oneclickvirt.goecs/lib",
49+
"/data/app/com.oneclickvirt.goecs/lib",
50+
}
51+
52+
for _, basePath := range possibleBasePaths {
53+
if info, err := os.Stat(basePath); err == nil && info.IsDir() {
54+
return basePath, nil
55+
}
56+
57+
// 尝试带哈希的路径(Android 5.0+)
58+
parent := filepath.Dir(basePath)
59+
parentEntries, err := os.ReadDir(parent)
60+
if err == nil {
61+
for _, entry := range parentEntries {
62+
if entry.IsDir() && strings.HasPrefix(entry.Name(), "com.oneclickvirt.goecs") {
63+
libDir := filepath.Join(parent, entry.Name(), "lib")
64+
if info, err := os.Stat(libDir); err == nil && info.IsDir() {
65+
return libDir, nil
66+
}
67+
}
68+
}
69+
}
70+
}
71+
72+
// 方法 3: 搜索 /data/app 目录
73+
dataAppDir := "/data/app"
74+
if entries, err := os.ReadDir(dataAppDir); err == nil {
75+
for _, entry := range entries {
76+
if entry.IsDir() && strings.Contains(entry.Name(), "com.oneclickvirt.goecs") {
77+
libDir := filepath.Join(dataAppDir, entry.Name(), "lib")
78+
if info, err := os.Stat(libDir); err == nil && info.IsDir() {
79+
return libDir, nil
80+
}
81+
}
82+
}
83+
}
84+
85+
return "", fmt.Errorf("无法找到 native library 目录")
86+
}
87+
88+
// getLibraryName 获取当前架构对应的库名称
89+
func getLibraryName() string {
90+
switch runtime.GOARCH {
91+
case "arm64":
92+
return "libgoecs_arm64.so"
93+
case "amd64":
94+
return "libgoecs_amd64.so"
95+
case "arm":
96+
return "libgoecs_arm.so"
97+
case "386":
98+
return "libgoecs_386.so"
99+
default:
100+
return "libgoecs.so"
101+
}
102+
}
103+
104+
// ExtractECSBinary 获取 ECS 二进制文件路径
105+
// 在 Android 上,我们不需要"提取",而是直接使用系统已安装的 native library
106+
func ExtractECSBinary() (string, error) {
107+
// 获取 native library 目录
108+
libDir, err := findNativeLibraryDir()
109+
if err != nil {
110+
return "", fmt.Errorf("获取 native library 目录失败: %v", err)
111+
}
112+
113+
// 尝试的文件名列表(按优先级)
114+
possibleNames := []string{
115+
"libgoecs.so", // 通用名称
116+
getLibraryName(), // 带架构后缀的名称
117+
}
118+
119+
// 尝试的子目录(Android ABI 名称)
120+
abiDirs := []string{
121+
"", // 直接在 lib 目录
122+
}
123+
124+
// 根据架构添加 ABI 目录
125+
switch runtime.GOARCH {
126+
case "arm64":
127+
abiDirs = append(abiDirs, "arm64-v8a", "arm64")
128+
case "arm":
129+
abiDirs = append(abiDirs, "armeabi-v7a", "armeabi", "arm")
130+
case "amd64":
131+
abiDirs = append(abiDirs, "x86_64", "x86-64")
132+
case "386":
133+
abiDirs = append(abiDirs, "x86")
134+
}
135+
136+
// 尝试所有可能的路径组合
137+
var checkedPaths []string
138+
for _, abiDir := range abiDirs {
139+
baseDir := libDir
140+
if abiDir != "" {
141+
baseDir = filepath.Join(libDir, abiDir)
142+
}
143+
144+
for _, name := range possibleNames {
145+
ecsPath := filepath.Join(baseDir, name)
146+
checkedPaths = append(checkedPaths, ecsPath)
147+
148+
if info, err := os.Stat(ecsPath); err == nil && !info.IsDir() {
149+
// 找到文件,确保有执行权限
150+
if err := os.Chmod(ecsPath, 0755); err != nil {
151+
// 在某些 Android 版本上可能无法修改权限,但这通常不是问题
152+
}
153+
return ecsPath, nil
154+
}
155+
}
156+
}
157+
158+
// 未找到文件,返回详细错误信息
159+
return "", fmt.Errorf("找不到 ECS 二进制文件\n已检查的路径:\n %s\n\n请确保:\n1. ECS 二进制文件已编译为 Android 版本\n2. 文件已放置在 jniLibs/%s/libgoecs.so\n3. APK 已重新打包",
160+
strings.Join(checkedPaths, "\n "),
161+
abiDirs[1]) // 显示推荐的 ABI 目录
162+
}
163+
164+
// CleanupECSBinary 清理函数
165+
// 在 Android 上,native library 由系统管理,我们不需要清理
166+
func CleanupECSBinary(path string) {
167+
// 不需要做任何事情
168+
// native library 由 Android 系统管理,应用卸载时会自动清理
169+
}

embedding/embed_universal.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//go:build !windows && !darwin
1+
//go:build !windows && !darwin && !android
22

33
package embedding
44

jniLibs/README.md

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# JNI Libraries 目录说明
2+
3+
这个目录用于存放 Android APK 中的 native libraries(ECS 二进制文件)。
4+
5+
## 目录结构
6+
7+
```
8+
jniLibs/
9+
├── arm64-v8a/ # ARM64 架构 (64位) - 主要目标
10+
│ └── libgoecs.so
11+
└── x86_64/ # x86_64 架构 (64位) - 模拟器
12+
└── libgoecs.so
13+
```
14+
15+
## 如何准备二进制文件
16+
17+
从 ECS 项目编译 Linux 二进制文件,然后复制并重命名为 `.so` 文件:
18+
19+
```bash
20+
# 1. 编译 Linux 二进制文件(使用 goreleaser 参数)
21+
cd /path/to/ecs
22+
23+
# ARM64
24+
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build \
25+
-ldflags="-s -w -X main.version=1.0.0 -X main.arch=arm64 -checklinkname=0" \
26+
-o goecs-linux-arm64 ./
27+
28+
# AMD64 (x86_64)
29+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
30+
-ldflags="-s -w -X main.version=1.0.0 -X main.arch=amd64 -checklinkname=0" \
31+
-o goecs-linux-amd64 ./
32+
33+
# 2. 复制到 jniLibs 目录并重命名为 .so
34+
cd /path/to/goecs
35+
cp /path/to/ecs/goecs-linux-arm64 jniLibs/arm64-v8a/libgoecs.so
36+
cp /path/to/ecs/goecs-linux-amd64 jniLibs/x86_64/libgoecs.so
37+
38+
# 3. 设置执行权限
39+
chmod 755 jniLibs/*/libgoecs.so
40+
```
41+
42+
## 工作原理
43+
44+
1. 在打包 APK 时,Fyne 会自动将 `jniLibs/` 目录中的文件打包进 APK
45+
2. Android 系统在安装 APK 时,会将这些 `.so` 文件提取到应用的 `nativeLibraryDir`(通常是 `/data/app/<package>/lib/<abi>/`
46+
3. 应用运行时,通过 `embedding/embed_android.go``nativeLibraryDir` 读取文件路径
47+
4. 使用 `exec.Command()` 直接执行该路径的二进制文件
48+
5. 这种方式不需要 root 权限,也不会触发 SELinux 限制
49+
50+
## 注意事项
51+
52+
- **文件必须命名为 `libgoecs.so`**(或其他以 `lib` 开头、`.so` 结尾的名称)
53+
- **使用 Linux 编译参数,不是 Android**:虽然目标是 Android,但使用 `GOOS=linux` 编译
54+
- 必须放在正确的 ABI 目录下(`arm64-v8a``x86_64`
55+
- 只需要这两个架构就够用了(覆盖真机和模拟器)
56+
57+
## 快速命令(一键准备)
58+
59+
假设 ECS 项目在 `../ecs`,当前在 `goecs` 项目根目录:
60+
61+
```bash
62+
# 编译并复制
63+
cd ../ecs && \
64+
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -checklinkname=0" -o goecs-linux-arm64 ./ && \
65+
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -checklinkname=0" -o goecs-linux-amd64 ./ && \
66+
cd ../goecs && \
67+
cp ../ecs/goecs-linux-arm64 jniLibs/arm64-v8a/libgoecs.so && \
68+
cp ../ecs/goecs-linux-amd64 jniLibs/x86_64/libgoecs.so && \
69+
chmod 755 jniLibs/*/libgoecs.so && \
70+
ls -lh jniLibs/*/libgoecs.so
71+
```
72+
73+
## 测试验证
74+
75+
### 1. 验证 APK 包含 .so 文件
76+
77+
```bash
78+
unzip -l your-app.apk | grep "lib/"
79+
# 应该看到:
80+
# lib/arm64-v8a/libgoecs.so
81+
# lib/x86_64/libgoecs.so
82+
```
83+
84+
### 2. 在设备上验证安装位置
85+
86+
```bash
87+
# 安装 APK
88+
adb install your-app.apk
89+
90+
# 查看安装位置
91+
adb shell pm path com.oneclickvirt.goecs
92+
93+
# 查看 native library 目录
94+
adb shell ls -l /data/app/com.oneclickvirt.goecs-*/lib/arm64/
95+
96+
# 测试执行
97+
adb shell
98+
run-as com.oneclickvirt.goecs
99+
cd /data/app/com.oneclickvirt.goecs-*/lib/arm64/
100+
./libgoecs.so --help
101+
```
102+
103+
### 3. 查看应用日志
104+
105+
```bash
106+
# 实时查看日志
107+
adb logcat | grep -E "(goecs|oneclickvirt)"
108+
109+
# 只看错误
110+
adb logcat *:E | grep goecs
111+
```
112+
113+
## 故障排除
114+
115+
如果看到 "fork/exec" 错误:
116+
117+
1. **检查文件是否存在**
118+
```bash
119+
adb shell run-as com.oneclickvirt.goecs find /data/app -name "libgoecs.so"
120+
```
121+
122+
2. **检查编译架构是否匹配设备**
123+
```bash
124+
adb shell getprop ro.product.cpu.abi
125+
# arm64-v8a -> 需要 jniLibs/arm64-v8a/libgoecs.so
126+
# x86_64 -> 需要 jniLibs/x86_64/libgoecs.so
127+
```
128+
129+
3. **验证文件不是空的**
130+
```bash
131+
ls -lh jniLibs/*/libgoecs.so
132+
# 文件应该有合理的大小(几MB到几十MB)
133+
```
134+
135+
4. **检查应用日志中的详细错误信息**
136+
- embedding 代码会输出所有检查过的路径
137+
- 查看 logcat 获取详细信息
138+

0 commit comments

Comments
 (0)