Files
RUSTDESK-AP-SERVER-SUNLIX/service/clientBuild.go

628 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/google/uuid"
"github.com/lejianwen/rustdesk-api/v2/global"
"github.com/lejianwen/rustdesk-api/v2/model"
"gorm.io/gorm"
)
type ClientBuildService struct {
}
// BuildConfig конфигурация для сборки клиента
type BuildConfig struct {
Platform string `json:"platform"`
Version string `json:"version"`
AppName string `json:"app_name"`
FileName string `json:"file_name"`
ServerIP string `json:"server_ip"`
ApiServer string `json:"api_server"`
Key string `json:"key"`
Custom map[string]string `json:"custom"` // дополнительные настройки
IconPath string `json:"icon_path,omitempty"`
LogoPath string `json:"logo_path,omitempty"`
CustomConfigFile string `json:"custom_config_file,omitempty"` // кастомный конфигурационный файл
UseCustomConfig bool `json:"use_custom_config"` // использовать кастомный конфиг
}
// CreateBuild создает новую задачу компиляции
func (cbs *ClientBuildService) CreateBuild(userId uint, config *BuildConfig) (*model.ClientBuild, error) {
buildUuid := uuid.New().String()
// Сохраняем конфигурацию в JSON
configJson, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("failed to marshal config: %v", err)
}
build := &model.ClientBuild{
UserId: userId,
BuildUuid: buildUuid,
Platform: config.Platform,
Version: config.Version,
AppName: config.AppName,
FileName: config.FileName,
Status: "pending",
BuildConfig: string(configJson),
}
if err := DB.Create(build).Error; err != nil {
return nil, fmt.Errorf("failed to create build: %v", err)
}
// Запускаем компиляцию в фоне
go cbs.buildClient(build)
return build, nil
}
// buildClient выполняет компиляцию клиента
func (cbs *ClientBuildService) buildClient(build *model.ClientBuild) {
// Обновляем статус на "building"
DB.Model(build).Update("status", "building")
DB.Model(build).Update("build_log", "Starting build process...\n")
// Парсим конфигурацию
var config BuildConfig
if err := json.Unmarshal([]byte(build.BuildConfig), &config); err != nil {
cbs.updateBuildStatus(build, "failed", fmt.Sprintf("Failed to parse config: %v", err), "")
return
}
// Создаем директорию для сборки
buildDir := filepath.Join(global.Config.Gin.ResourcesPath, "builds", build.BuildUuid)
if err := os.MkdirAll(buildDir, 0755); err != nil {
cbs.updateBuildStatus(build, "failed", fmt.Sprintf("Failed to create build directory: %v", err), "")
return
}
// Выполняем компиляцию в зависимости от платформы
var err error
var outputPath string
switch config.Platform {
case "windows":
outputPath, err = cbs.buildWindows(build, config, buildDir)
case "linux":
outputPath, err = cbs.buildLinux(build, config, buildDir)
case "macos", "macos-x86":
outputPath, err = cbs.buildMacOS(build, config, buildDir)
case "android":
outputPath, err = cbs.buildAndroid(build, config, buildDir)
default:
err = fmt.Errorf("unsupported platform: %s", config.Platform)
}
if err != nil {
cbs.updateBuildStatus(build, "failed", err.Error(), "")
return
}
// Получаем размер файла
fileInfo, err := os.Stat(outputPath)
if err != nil {
cbs.updateBuildStatus(build, "failed", fmt.Sprintf("Failed to get file info: %v", err), "")
return
}
// Обновляем имя файла, если оно не соответствует расширению
fileName := build.FileName
// Получаем расширение из пути
if len(outputPath) > 4 {
ext := filepath.Ext(outputPath)
if ext != "" && !strings.HasSuffix(strings.ToLower(fileName), strings.ToLower(ext)) {
// Обновляем расширение
if strings.Contains(fileName, ".") {
fileName = fileName[:strings.LastIndex(fileName, ".")] + ext
} else {
fileName = fileName + ext
}
}
}
// Обновляем статус на успех
DB.Model(build).Updates(map[string]interface{}{
"status": "success",
"file_path": outputPath,
"file_name": fileName,
"file_size": fileInfo.Size(),
"build_log": build.BuildLog + "\nBuild completed successfully!",
})
}
// cloneRustDeskSource клонирует исходный код RustDesk
func (cbs *ClientBuildService) cloneRustDeskSource(build *model.ClientBuild, version string, sourceDir string) error {
cbs.appendLog(build, fmt.Sprintf("Cloning RustDesk source code (version: %s)...\n", version))
// Проверяем, существует ли уже клонированный репозиторий
if _, err := os.Stat(filepath.Join(sourceDir, ".git")); err == nil {
cbs.appendLog(build, "Repository already exists, updating...\n")
// Обновляем существующий репозиторий
cmd := exec.Command("git", "fetch", "origin")
cmd.Dir = sourceDir
if output, err := cmd.CombinedOutput(); err != nil {
cbs.appendLog(build, fmt.Sprintf("Warning: failed to fetch updates: %s\n", string(output)))
}
// Обновляем submodules
cbs.appendLog(build, "Updating submodules...\n")
cmd = exec.Command("git", "submodule", "update", "--init", "--recursive")
cmd.Dir = sourceDir
if output, err := cmd.CombinedOutput(); err != nil {
cbs.appendLog(build, fmt.Sprintf("Warning: failed to update submodules: %s\n", string(output)))
} else {
cbs.appendLog(build, "Submodules updated successfully\n")
}
} else {
// Клонируем репозиторий
cbs.appendLog(build, "Cloning repository...\n")
normalizedVersion := strings.TrimPrefix(version, "v")
// Клонируем репозиторий (без --depth 1, чтобы получить все файлы и submodules)
cmd := exec.Command("git", "clone", "--recursive", "https://github.com/rustdesk/rustdesk.git", sourceDir)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to clone repository: %v\nOutput: %s", err, string(output))
}
cbs.appendLog(build, "Repository cloned successfully\n")
// Переключаемся на нужную версию, если указана
if normalizedVersion != "master" && normalizedVersion != "" {
cbs.appendLog(build, fmt.Sprintf("Checking out version: %s\n", normalizedVersion))
cmd := exec.Command("git", "checkout", normalizedVersion)
cmd.Dir = sourceDir
if output, err := cmd.CombinedOutput(); err != nil {
// Пробуем с префиксом v
cmd = exec.Command("git", "checkout", "v"+normalizedVersion)
cmd.Dir = sourceDir
if output2, err2 := cmd.CombinedOutput(); err2 != nil {
return fmt.Errorf("failed to checkout version %s: %v\nOutput: %s\nTried v%s: %s", normalizedVersion, err, string(output), normalizedVersion, string(output2))
}
}
cbs.appendLog(build, fmt.Sprintf("Checked out version: %s\n", normalizedVersion))
// Обновляем submodules для конкретной версии
cbs.appendLog(build, "Updating submodules...\n")
cmd = exec.Command("git", "submodule", "update", "--init", "--recursive")
cmd.Dir = sourceDir
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to update submodules: %v\nOutput: %s", err, string(output))
}
cbs.appendLog(build, "Submodules updated successfully\n")
} else {
// Для master обновляем submodules
cbs.appendLog(build, "Updating submodules...\n")
cmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
cmd.Dir = sourceDir
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to update submodules: %v\nOutput: %s", err, string(output))
}
cbs.appendLog(build, "Submodules updated successfully\n")
}
}
return nil
}
// copyFile копирует файл из источника в назначение
func (cbs *ClientBuildService) copyFile(src, dst string) error {
sourceFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source file: %v", err)
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("failed to create destination file: %v", err)
}
defer destFile.Close()
_, err = io.Copy(destFile, sourceFile)
if err != nil {
return fmt.Errorf("failed to copy file: %v", err)
}
return nil
}
// buildWindows компилирует клиент для Windows
func (cbs *ClientBuildService) buildWindows(build *model.ClientBuild, config BuildConfig, buildDir string) (string, error) {
cbs.appendLog(build, "Building Windows client from source...\n")
// Проверяем наличие Rust toolchain
if _, err := exec.LookPath("cargo"); err != nil {
return "", fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
}
// Создаем директорию для исходников
sourceDir := filepath.Join(buildDir, "rustdesk-source")
if err := os.MkdirAll(sourceDir, 0755); err != nil {
return "", fmt.Errorf("failed to create source directory: %v", err)
}
// Клонируем исходный код
if err := cbs.cloneRustDeskSource(build, config.Version, sourceDir); err != nil {
return "", fmt.Errorf("failed to clone source: %v", err)
}
// Создаем конфигурационный файл custom.txt
var customConfig string
if config.UseCustomConfig && config.CustomConfigFile != "" {
customConfig = config.CustomConfigFile
cbs.appendLog(build, "Using custom configuration file\n")
} else {
customConfig = cbs.generateCustomConfig(config)
cbs.appendLog(build, "Using auto-generated configuration\n")
}
// Записываем custom.txt в исходники
customPath := filepath.Join(sourceDir, "custom.txt")
if err := os.WriteFile(customPath, []byte(customConfig), 0644); err != nil {
return "", fmt.Errorf("failed to write custom.txt: %v", err)
}
// Компилируем
cbs.appendLog(build, "Compiling Windows client with cargo...\n")
cmd := exec.Command("cargo", "build", "--release", "--target", "x86_64-pc-windows-msvc")
cmd.Dir = sourceDir
cmd.Env = append(os.Environ(), "RUSTFLAGS=-C target-feature=+crt-static")
output, err := cmd.CombinedOutput()
cbs.appendLog(build, string(output))
if err != nil {
return "", fmt.Errorf("build failed: %v", err)
}
// Копируем скомпилированный файл
exePath := filepath.Join(sourceDir, "target", "x86_64-pc-windows-msvc", "release", "rustdesk.exe")
if _, err := os.Stat(exePath); os.IsNotExist(err) {
return "", fmt.Errorf("compiled file not found: %s", exePath)
}
// Определяем имя выходного файла
outputFileName := config.FileName
if !strings.HasSuffix(strings.ToLower(outputFileName), ".exe") {
outputFileName = outputFileName + ".exe"
}
outputPath := filepath.Join(buildDir, outputFileName)
if err := cbs.copyFile(exePath, outputPath); err != nil {
return "", fmt.Errorf("failed to copy compiled file: %v", err)
}
// Копируем custom.txt рядом с исполняемым файлом
customOutputPath := filepath.Join(buildDir, "custom.txt")
if err := os.WriteFile(customOutputPath, []byte(customConfig), 0644); err != nil {
cbs.appendLog(build, fmt.Sprintf("Warning: failed to copy custom.txt: %v\n", err))
}
cbs.appendLog(build, fmt.Sprintf("Windows client compiled successfully: %s\n", outputFileName))
return outputPath, nil
}
// buildLinux компилирует клиент для Linux
func (cbs *ClientBuildService) buildLinux(build *model.ClientBuild, config BuildConfig, buildDir string) (string, error) {
cbs.appendLog(build, "Building Linux client from source...\n")
// Проверяем наличие Rust toolchain
if _, err := exec.LookPath("cargo"); err != nil {
return "", fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
}
// Создаем директорию для исходников
sourceDir := filepath.Join(buildDir, "rustdesk-source")
if err := os.MkdirAll(sourceDir, 0755); err != nil {
return "", fmt.Errorf("failed to create source directory: %v", err)
}
// Клонируем исходный код
if err := cbs.cloneRustDeskSource(build, config.Version, sourceDir); err != nil {
return "", fmt.Errorf("failed to clone source: %v", err)
}
// Создаем конфигурационный файл custom.txt
var customConfig string
if config.UseCustomConfig && config.CustomConfigFile != "" {
customConfig = config.CustomConfigFile
cbs.appendLog(build, "Using custom configuration file\n")
} else {
customConfig = cbs.generateCustomConfig(config)
cbs.appendLog(build, "Using auto-generated configuration\n")
}
// Записываем custom.txt в исходники
customPath := filepath.Join(sourceDir, "custom.txt")
if err := os.WriteFile(customPath, []byte(customConfig), 0644); err != nil {
return "", fmt.Errorf("failed to write custom.txt: %v", err)
}
// Компилируем
cbs.appendLog(build, "Compiling Linux client with cargo...\n")
cmd := exec.Command("cargo", "build", "--release", "--target", "x86_64-unknown-linux-gnu")
cmd.Dir = sourceDir
output, err := cmd.CombinedOutput()
cbs.appendLog(build, string(output))
if err != nil {
return "", fmt.Errorf("build failed: %v", err)
}
// Копируем скомпилированный файл
binPath := filepath.Join(sourceDir, "target", "x86_64-unknown-linux-gnu", "release", "rustdesk")
if _, err := os.Stat(binPath); os.IsNotExist(err) {
return "", fmt.Errorf("compiled file not found: %s", binPath)
}
// Определяем имя выходного файла (без расширения для Linux бинарника)
outputFileName := config.FileName
if strings.HasSuffix(strings.ToLower(outputFileName), ".deb") || strings.HasSuffix(strings.ToLower(outputFileName), ".rpm") {
// Если указано расширение пакета, оставляем как есть, но создаем бинарник
outputFileName = strings.TrimSuffix(outputFileName, filepath.Ext(outputFileName))
}
outputPath := filepath.Join(buildDir, outputFileName)
if err := cbs.copyFile(binPath, outputPath); err != nil {
return "", fmt.Errorf("failed to copy compiled file: %v", err)
}
// Делаем файл исполняемым
if err := os.Chmod(outputPath, 0755); err != nil {
cbs.appendLog(build, fmt.Sprintf("Warning: failed to set executable permissions: %v\n", err))
}
// Копируем custom.txt рядом с исполняемым файлом
customOutputPath := filepath.Join(buildDir, "custom.txt")
if err := os.WriteFile(customOutputPath, []byte(customConfig), 0644); err != nil {
cbs.appendLog(build, fmt.Sprintf("Warning: failed to copy custom.txt: %v\n", err))
}
cbs.appendLog(build, fmt.Sprintf("Linux client compiled successfully: %s\n", outputFileName))
return outputPath, nil
}
// buildMacOS компилирует клиент для macOS
func (cbs *ClientBuildService) buildMacOS(build *model.ClientBuild, config BuildConfig, buildDir string) (string, error) {
cbs.appendLog(build, "Building macOS client from source...\n")
// Проверяем наличие Rust toolchain
if _, err := exec.LookPath("cargo"); err != nil {
return "", fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
}
// Определяем целевую архитектуру
target := "x86_64-apple-darwin"
if config.Platform == "macos" {
// Для Apple Silicon
target = "aarch64-apple-darwin"
}
// Создаем директорию для исходников
sourceDir := filepath.Join(buildDir, "rustdesk-source")
if err := os.MkdirAll(sourceDir, 0755); err != nil {
return "", fmt.Errorf("failed to create source directory: %v", err)
}
// Клонируем исходный код
if err := cbs.cloneRustDeskSource(build, config.Version, sourceDir); err != nil {
return "", fmt.Errorf("failed to clone source: %v", err)
}
// Создаем конфигурационный файл custom.txt
var customConfig string
if config.UseCustomConfig && config.CustomConfigFile != "" {
customConfig = config.CustomConfigFile
cbs.appendLog(build, "Using custom configuration file\n")
} else {
customConfig = cbs.generateCustomConfig(config)
cbs.appendLog(build, "Using auto-generated configuration\n")
}
// Записываем custom.txt в исходники
customPath := filepath.Join(sourceDir, "custom.txt")
if err := os.WriteFile(customPath, []byte(customConfig), 0644); err != nil {
return "", fmt.Errorf("failed to write custom.txt: %v", err)
}
// Компилируем
cbs.appendLog(build, fmt.Sprintf("Compiling macOS client with cargo (target: %s)...\n", target))
cmd := exec.Command("cargo", "build", "--release", "--target", target)
cmd.Dir = sourceDir
output, err := cmd.CombinedOutput()
cbs.appendLog(build, string(output))
if err != nil {
return "", fmt.Errorf("build failed: %v", err)
}
// Копируем скомпилированный файл
binPath := filepath.Join(sourceDir, "target", target, "release", "rustdesk")
if _, err := os.Stat(binPath); os.IsNotExist(err) {
return "", fmt.Errorf("compiled file not found: %s", binPath)
}
// Определяем имя выходного файла
outputFileName := config.FileName
if !strings.HasSuffix(strings.ToLower(outputFileName), ".app") && !strings.HasSuffix(strings.ToLower(outputFileName), ".dmg") {
outputFileName = outputFileName + ".app"
}
outputPath := filepath.Join(buildDir, outputFileName)
// Для macOS создаем .app bundle (упрощенная версия)
// В реальности нужно создать полноценный .app bundle, но для начала просто копируем бинарник
if err := cbs.copyFile(binPath, outputPath); err != nil {
return "", fmt.Errorf("failed to copy compiled file: %v", err)
}
// Делаем файл исполняемым
if err := os.Chmod(outputPath, 0755); err != nil {
cbs.appendLog(build, fmt.Sprintf("Warning: failed to set executable permissions: %v\n", err))
}
// Копируем custom.txt рядом с исполняемым файлом
customOutputPath := filepath.Join(buildDir, "custom.txt")
if err := os.WriteFile(customOutputPath, []byte(customConfig), 0644); err != nil {
cbs.appendLog(build, fmt.Sprintf("Warning: failed to copy custom.txt: %v\n", err))
}
cbs.appendLog(build, fmt.Sprintf("macOS client compiled successfully: %s\n", outputFileName))
return outputPath, nil
}
// buildAndroid компилирует клиент для Android
func (cbs *ClientBuildService) buildAndroid(build *model.ClientBuild, config BuildConfig, buildDir string) (string, error) {
cbs.appendLog(build, "Building Android client from source...\n")
// Проверяем наличие Rust toolchain
if _, err := exec.LookPath("cargo"); err != nil {
return "", fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
}
// Проверяем наличие Android SDK
if os.Getenv("ANDROID_HOME") == "" && os.Getenv("ANDROID_SDK_ROOT") == "" {
return "", fmt.Errorf("Android SDK not found. Please set ANDROID_HOME or ANDROID_SDK_ROOT environment variable")
}
cbs.appendLog(build, "Android compilation requires additional setup (NDK, etc.)\n")
cbs.appendLog(build, "This is a placeholder - full Android build requires Android NDK and Gradle setup\n")
// Для Android компиляция более сложная и требует настройки NDK
// Пока возвращаем ошибку с инструкциями
return "", fmt.Errorf("Android compilation from source requires Android NDK setup. Please refer to RustDesk documentation for Android build instructions")
}
// generateCustomConfig генерирует конфигурационный файл custom.txt
func (cbs *ClientBuildService) generateCustomConfig(config BuildConfig) string {
var lines []string
// Базовые настройки сервера
if config.ServerIP != "" {
lines = append(lines, fmt.Sprintf("rendezvous-server = %s", config.ServerIP))
}
if config.ApiServer != "" {
lines = append(lines, fmt.Sprintf("api-server = %s", config.ApiServer))
}
if config.Key != "" {
lines = append(lines, fmt.Sprintf("key = %s", config.Key))
}
if config.AppName != "" && strings.ToLower(config.AppName) != "rustdesk" {
lines = append(lines, fmt.Sprintf("app-name = %s", config.AppName))
}
// Добавляем дополнительные настройки из custom
for key, value := range config.Custom {
lines = append(lines, fmt.Sprintf("%s = %s", key, value))
}
return strings.Join(lines, "\n")
}
// updateBuildStatus обновляет статус сборки
func (cbs *ClientBuildService) updateBuildStatus(build *model.ClientBuild, status, errorMsg, log string) {
updates := map[string]interface{}{
"status": status,
}
if errorMsg != "" {
updates["error_msg"] = errorMsg
}
if log != "" {
updates["build_log"] = build.BuildLog + "\n" + log
}
DB.Model(build).Updates(updates)
}
// appendLog добавляет строку в лог сборки
func (cbs *ClientBuildService) appendLog(build *model.ClientBuild, logLine string) {
var currentBuild model.ClientBuild
DB.First(&currentBuild, build.Id)
newLog := currentBuild.BuildLog + logLine
DB.Model(build).Update("build_log", newLog)
build.BuildLog = newLog
}
// GetByUuid получает задачу компиляции по UUID
func (cbs *ClientBuildService) GetByUuid(uuid string) *model.ClientBuild {
build := &model.ClientBuild{}
DB.Where("build_uuid = ?", uuid).First(build)
return build
}
// GetByUserId получает список задач компиляции пользователя
func (cbs *ClientBuildService) GetByUserId(userId uint) []*model.ClientBuild {
var builds []*model.ClientBuild
DB.Where("user_id = ?", userId).Order("created_at DESC").Find(&builds)
return builds
}
// List получает список задач компиляции с пагинацией
func (cbs *ClientBuildService) List(page, pageSize uint, scopes ...func(*gorm.DB) *gorm.DB) *model.ClientBuildList {
var builds []*model.ClientBuild
var total int64
query := DB.Model(&model.ClientBuild{})
for _, scope := range scopes {
query = scope(query)
}
query.Count(&total)
query = Paginate(page, pageSize)(query)
query.Order("created_at DESC").Find(&builds)
return &model.ClientBuildList{
ClientBuilds: builds,
Pagination: model.Pagination{
Page: int64(page),
PageSize: int64(pageSize),
Total: total,
},
}
}
// Delete удаляет задачу компиляции
func (cbs *ClientBuildService) Delete(id, userId uint) error {
build := &model.ClientBuild{}
DB.Where("id = ? AND user_id = ?", id, userId).First(build)
if build.Id == 0 {
return fmt.Errorf("build not found")
}
// Удаляем файлы сборки
if build.FilePath != "" {
os.Remove(build.FilePath)
// Удаляем директорию сборки
buildDir := filepath.Dir(build.FilePath)
os.RemoveAll(buildDir)
}
return DB.Delete(build).Error
}
// checkBuildTools проверяет наличие инструментов для сборки
func (cbs *ClientBuildService) checkBuildTools(platform string) error {
switch platform {
case "windows", "linux", "macos":
// Проверяем наличие Rust
if _, err := exec.LookPath("cargo"); err != nil {
return fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
}
case "android":
// Проверяем наличие Android SDK
if os.Getenv("ANDROID_HOME") == "" {
return fmt.Errorf("Android SDK not found. Please set ANDROID_HOME environment variable")
}
}
return nil
}