新增应用打包模块

This commit is contained in:
zyj
2025-11-24 11:45:42 +08:00
parent f393d2b1de
commit 51cfd5cf21
5 changed files with 740 additions and 51 deletions

244
PACK_GUIDE.md Normal file
View File

@@ -0,0 +1,244 @@
# 网站打包成应用指南
## 功能说明
本工具可以将静态网站打包成独立的桌面应用程序,无需用户安装浏览器或其他运行环境。
## 打包原理
1. **创建 Wails 项目**:自动生成一个新的 Wails3 项目结构
2. **嵌入网站文件**将静态网站文件HTML、CSS、JS等嵌入到应用中
3. **编译应用**:使用 Wails3 编译生成桌面应用
4. **独立运行**:打包后的应用内置 WebView无需浏览器
## 环境要求
### 必需环境
- **Go 1.21+**Go 编程语言环境
- **Wails3**:桌面应用框架
### 安装方式
#### 方式一:使用本工具安装(推荐)
1. 进入"应用打包"页面
2. 查看环境检查卡片
3. 点击"安装 Go"和"安装 Wails3"按钮
4. 等待安装完成
#### 方式二:手动安装
**安装 Go**
```powershell
# 下载 Go 安装包
# https://go.dev/dl/
# 或使用工具提供的脚本
.\install\go.bat
```
**安装 Wails3**
```powershell
# 确保已安装 Go
go install github.com/wailsapp/wails/v3/cmd/wails3@latest
# 或使用工具提供的脚本
.\install\wails.bat
```
## 使用步骤
### 1. 准备网站文件
确保您的网站文件夹包含:
- `index.html`(必需,作为应用入口)
- CSS 文件
- JavaScript 文件
- 图片、字体等资源文件
示例结构:
```
my-website/
├── index.html
├── css/
│ └── style.css
├── js/
│ └── app.js
└── images/
└── logo.png
```
### 2. 选择网站
- 点击"选择文件夹"按钮
- 选择包含网站文件的文件夹
- 或从已下载的网站列表中选择
### 3. 配置应用信息
填写应用基本信息:
- **应用名称**(必填):应用的显示名称
- **版本号**:如 1.0.0
- **作者**:您的名字或团队名称
- **应用描述**:简短描述应用功能
### 4. 打包设置
配置打包参数:
- **目标平台**:选择 Windows、macOS 或 Linux
- **窗口宽度**:默认 1280px
- **窗口高度**:默认 800px
- **输出目录**:可自定义,默认为 `site-dist`
### 5. 开始打包
- 确认所有配置正确
- 点击"开始打包"按钮
- 等待编译完成首次可能需要5-10分钟
## 打包输出
### 文件位置
默认输出到项目的 `site-dist` 目录,包含:
- Windows`your-app.exe`
- macOS`your-app.app`
- Linux`your-app`(可执行文件)
### 文件大小
打包后的应用大小取决于:
- 网站文件大小
- 平台差异
- 通常在 20-50MB 之间
## 注意事项
### 首次打包
- 需要下载 Go 依赖包
- 可能耗时 5-10 分钟
- 需要网络连接
- 建议有至少 500MB 可用磁盘空间
### 网站要求
- **纯静态网站**:支持 HTML、CSS、JavaScript
- **不支持服务端代码**:如 PHP、Python 等
- **相对路径**:建议使用相对路径引用资源
- **入口文件**:必须有 index.html
### 资源引用
在 HTML 中使用相对路径:
```html
<!-- 推荐 -->
<link rel="stylesheet" href="./css/style.css">
<script src="./js/app.js"></script>
<img src="./images/logo.png">
<!-- 不推荐 -->
<link rel="stylesheet" href="/css/style.css">
<script src="http://example.com/js/app.js"></script>
```
### 跨域问题
打包后的应用使用 `file://` 协议,可能遇到跨域限制:
- 避免使用外部 APICORS 限制)
- 避免使用绝对路径
- 将所有资源放在网站文件夹内
## 常见问题
### Q: 打包失败怎么办?
A: 检查以下几点:
1. 确保 Go 和 Wails3 已正确安装
2. 检查网络连接(首次打包需要下载依赖)
3. 确保磁盘空间充足
4. 查看错误信息,根据提示解决
### Q: 打包的应用很大?
A: 这是正常的,因为:
1. 包含了 WebView 运行时
2. 包含了 Go 运行时
3. 包含了所有网站文件
4. 可以通过压缩网站资源减小体积
### Q: 应用可以在其他电脑运行吗?
A: 可以!打包后的应用是独立的:
- Windows 应用可在任何 Windows 7+ 系统运行
- macOS 应用可在任何 macOS 10.13+ 系统运行
- Linux 应用可在大多数 Linux 发行版运行
- 无需安装 Go、Wails 或浏览器
### Q: 如何更新应用?
A: 重新打包即可:
1. 修改网站文件
2. 更改版本号
3. 重新执行打包流程
4. 分发新的可执行文件
### Q: 可以添加自定义图标吗?
A: 当前版本暂不支持,后续版本会添加此功能。
## 高级用法
### 自定义配置
编辑生成的项目:
1. 打包时会在临时目录创建项目
2. 可以在 `main.go` 中自定义窗口行为
3. 可以添加 Go 后端代码
### 批量打包
使用命令行方式:
```powershell
# 切换到临时项目目录
cd C:\Users\{user}\AppData\Local\Temp\wails-pack-{app-name}
# 执行构建
wails3 build
```
## 技术架构
```
┌─────────────────────────────────────┐
│ 桌面应用 (.exe) │
├─────────────────────────────────────┤
│ Wails3 框架 │
│ ├─ WebView (渲染引擎) │
│ ├─ Go 运行时 │
│ └─ 嵌入的网站文件 │
│ ├─ HTML │
│ ├─ CSS │
│ ├─ JavaScript │
│ └─ 其他资源 │
└─────────────────────────────────────┘
```
## 支持的平台
| 平台 | 架构 | 输出格式 | 备注 |
|------|------|---------|------|
| Windows | x64 | .exe | Windows 7+ |
| macOS | x64/arm64 | .app | macOS 10.13+ |
| Linux | x64 | 可执行文件 | 主流发行版 |
## 更多信息
- Wails 官方文档https://wails.io
- Go 官方网站https://go.dev
- 项目 GitHub待添加
## 许可证
本工具遵循 MIT 许可证。

120
app.go
View File

@@ -556,16 +556,35 @@ func (a *App) PackApp(packConfig map[string]interface{}) error {
// 获取配置参数 // 获取配置参数
sitePath, _ := packConfig["sitePath"].(string) sitePath, _ := packConfig["sitePath"].(string)
appName, _ := packConfig["appName"].(string) appName, _ := packConfig["appName"].(string)
version, _ := packConfig["version"].(string)
author, _ := packConfig["author"].(string)
description, _ := packConfig["description"].(string)
outputDir, _ := packConfig["outputDir"].(string) outputDir, _ := packConfig["outputDir"].(string)
width, _ := packConfig["width"].(float64)
height, _ := packConfig["height"].(float64)
// 平台参数处理
platformsInterface, _ := packConfig["platforms"].([]interface{})
platforms := make([]string, 0)
for _, p := range platformsInterface {
if platform, ok := p.(string); ok {
platforms = append(platforms, platform)
}
}
if sitePath == "" || appName == "" { if sitePath == "" || appName == "" {
return fmt.Errorf("缺少必要参数") return fmt.Errorf("缺少必要参数: 网站路径和应用名称不能为空")
}
// 检查网站路径是否存在
if _, err := os.Stat(sitePath); os.IsNotExist(err) {
return fmt.Errorf("网站路径不存在: %s", sitePath)
} }
// 发送打包进度 // 发送打包进度
a.app.Event.Emit("pack:progress", map[string]interface{}{ a.app.Event.Emit("pack:progress", map[string]interface{}{
"step": "preparing", "step": "preparing",
"percent": 10, "percent": 5,
"message": "正在准备打包环境...", "message": "正在准备打包环境...",
}) })
@@ -576,15 +595,25 @@ func (a *App) PackApp(packConfig map[string]interface{}) error {
} }
if !envStatus.HasGo || !envStatus.HasWails { if !envStatus.HasGo || !envStatus.HasWails {
return fmt.Errorf("缺少必要的环境: Go=%v, Wails=%v", envStatus.HasGo, envStatus.HasWails) return fmt.Errorf("缺少必要的环境: Go=%v, Wails=%v。请先安装开发环境", envStatus.HasGo, envStatus.HasWails)
} }
// 创建临时项目目录 // 设置默认值
a.app.Event.Emit("pack:progress", map[string]interface{}{ if version == "" {
"step": "creating", version = "1.0.0"
"percent": 30, }
"message": "正在创建项目结构...", if description == "" {
}) description = appName
}
if len(platforms) == 0 {
platforms = []string{"windows"}
}
if width == 0 {
width = 1280
}
if height == 0 {
height = 800
}
// 设置输出目录 // 设置输出目录
if outputDir == "" { if outputDir == "" {
@@ -595,8 +624,12 @@ func (a *App) PackApp(packConfig map[string]interface{}) error {
} }
} }
// 确保输出目录存在 // 创建临时项目目录
os.MkdirAll(outputDir, 0755) a.app.Event.Emit("pack:progress", map[string]interface{}{
"step": "creating",
"percent": 15,
"message": "正在创建Wails项目结构...",
})
tempDir := filepath.Join(os.TempDir(), "wails-pack-"+appName) tempDir := filepath.Join(os.TempDir(), "wails-pack-"+appName)
os.RemoveAll(tempDir) // 清理旧的 os.RemoveAll(tempDir) // 清理旧的
@@ -605,48 +638,63 @@ func (a *App) PackApp(packConfig map[string]interface{}) error {
return fmt.Errorf("创建临时目录失败: %v", err) return fmt.Errorf("创建临时目录失败: %v", err)
} }
// TODO: 创建Wails项目结构,复制网站文件等 // 使用打包服务创建项目
// 这里是完整打包逻辑的占位符 packService := &services.PackService{}
// 实际需要: config := services.PackConfig{
// 1. 创建wails项目结构 (go.mod, main.go等) SitePath: sitePath,
// 2. 复制网站文件到项目的assets目录 AppName: appName,
// 3. 配置wails项目参数 Version: version,
Author: author,
Description: description,
Platforms: platforms,
Width: int(width),
Height: int(height),
OutputDir: outputDir,
}
log.Printf("准备打包网站: %s 到应用: %s", sitePath, appName) log.Printf("开始打包网站: %s", sitePath)
log.Printf("应用名称: %s", appName)
log.Printf("临时目录: %s", tempDir) log.Printf("临时目录: %s", tempDir)
log.Printf("输出目录: %s", outputDir) log.Printf("输出目录: %s", outputDir)
a.app.Event.Emit("pack:progress", map[string]interface{}{ a.app.Event.Emit("pack:progress", map[string]interface{}{
"step": "building", "step": "copying",
"percent": 60, "percent": 30,
"message": "正在编译应用...", "message": "正在复制网站文件...",
}) })
// 使用Wails构 // 创建Wails项目结
wailsCmd := envStatus.WailsPath if err := packService.CreateWailsProject(config, tempDir); err != nil {
if wailsCmd == "" { a.app.Event.Emit("pack:progress", map[string]interface{}{
wailsCmd = "wails3" "step": "error",
"percent": 0,
"error": fmt.Sprintf("创建项目失败: %v", err),
})
return fmt.Errorf("创建Wails项目失败: %v", err)
} }
// 模拟构建过程(实际项目中这里应该真正调用wails build)
// cmd := exec.Command(wailsCmd, "build", "-o", filepath.Join(outputDir, appName+".exe"))
// cmd.Dir = tempDir
// 暂时模拟成功
a.app.Event.Emit("pack:progress", map[string]interface{}{ a.app.Event.Emit("pack:progress", map[string]interface{}{
"step": "packaging", "step": "building",
"percent": 80, "percent": 50,
"message": "正在生成安装包...", "message": "正在编译应用(这可能需要几分钟)...",
}) })
// 模拟延迟 // 构建应用
// time.Sleep(2 * time.Second) if err := packService.BuildWailsApp(tempDir, outputDir, platforms); err != nil {
a.app.Event.Emit("pack:progress", map[string]interface{}{
"step": "error",
"percent": 0,
"error": fmt.Sprintf("构建失败: %v", err),
})
return fmt.Errorf("构建应用失败: %v", err)
}
a.app.Event.Emit("pack:progress", map[string]interface{}{ a.app.Event.Emit("pack:progress", map[string]interface{}{
"step": "completed", "step": "completed",
"percent": 100, "percent": 100,
"message": fmt.Sprintf("打包完成! 输出目录: %s", outputDir), "message": fmt.Sprintf("打包完成! 应用已保存到: %s", outputDir),
}) })
log.Printf("打包完成! 输出目录: %s", outputDir)
return nil return nil
} }

View File

@@ -33,12 +33,7 @@ tasks:
package: package:
summary: Packages a production build of the application summary: Packages a production build of the application
cmds: cmds:
- |- - task: create:nsis:installer
if [ "{{.FORMAT | default "nsis"}}" = "msix" ]; then
task: create:msix:package
else
task: create:nsis:installer
fi
vars: vars:
FORMAT: '{{.FORMAT | default "nsis"}}' FORMAT: '{{.FORMAT | default "nsis"}}'

View File

@@ -345,9 +345,15 @@
<h3 class="step-title">准备打包</h3> <h3 class="step-title">准备打包</h3>
<div class="pack-summary"> <div class="pack-summary">
<a-descriptions :column="1" bordered> <a-descriptions :column="1" bordered>
<a-descriptions-item label="网站名称">{{ selectedSiteName }}</a-descriptions-item> <a-descriptions-item label="网站路径">
<a-typography-paragraph :copyable="{ text: packConfig.sitePath }" style="margin: 0;">
{{ packConfig.sitePath }}
</a-typography-paragraph>
</a-descriptions-item>
<a-descriptions-item label="应用名称">{{ appConfig.name }}</a-descriptions-item> <a-descriptions-item label="应用名称">{{ appConfig.name }}</a-descriptions-item>
<a-descriptions-item label="版本号">{{ appConfig.version }}</a-descriptions-item> <a-descriptions-item label="版本号">{{ appConfig.version }}</a-descriptions-item>
<a-descriptions-item label="作者">{{ appConfig.author || '未填写' }}</a-descriptions-item>
<a-descriptions-item label="描述">{{ appConfig.description || '未填写' }}</a-descriptions-item>
<a-descriptions-item label="目标平台"> <a-descriptions-item label="目标平台">
<a-tag v-for="platform in packConfig.platforms" :key="platform" color="blue"> <a-tag v-for="platform in packConfig.platforms" :key="platform" color="blue">
{{ platform }} {{ platform }}
@@ -356,14 +362,29 @@
<a-descriptions-item label="窗口尺寸"> <a-descriptions-item label="窗口尺寸">
{{ packConfig.width }} x {{ packConfig.height }} {{ packConfig.width }} x {{ packConfig.height }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="输出目录">
<a-typography-paragraph :copyable="{ text: packConfig.outputDir || 'site-dist' }" style="margin: 0;">
{{ packConfig.outputDir || 'site-dist (默认)' }}
</a-typography-paragraph>
</a-descriptions-item>
</a-descriptions> </a-descriptions>
<a-alert
v-if="!envStatus.hasGo || !envStatus.hasWails"
type="error"
message="环境检查失败"
description="打包需要 Go 和 Wails3 环境,请返回第一步安装所需环境。"
show-icon
style="margin-top: 16px;"
/>
<div class="pack-action"> <div class="pack-action">
<a-button <a-button
type="primary" type="primary"
size="large" size="large"
:loading="packing" :loading="packing"
@click="startPacking" @click="startPacking"
:disabled="!envStatus.hasGo || !envStatus.hasWails"
block block
> >
<template #icon><rocket-outlined /></template> <template #icon><rocket-outlined /></template>
@@ -372,9 +393,21 @@
</div> </div>
<!-- 打包进度 --> <!-- 打包进度 -->
<div v-if="packing" class="pack-progress"> <div v-if="packing || packProgress > 0" class="pack-progress">
<a-progress :percent="packProgress" status="active" /> <a-progress
:percent="packProgress"
:status="packProgress === 100 ? 'success' : 'active'"
/>
<p class="progress-text">{{ packProgressText }}</p> <p class="progress-text">{{ packProgressText }}</p>
<a-alert
v-if="packProgress < 100 && packProgress > 0"
type="info"
message="打包提示"
description="首次打包需要下载依赖包可能需要5-10分钟请耐心等待..."
show-icon
closable
style="margin-top: 12px;"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -410,16 +443,32 @@
<div class="help-content"> <div class="help-content">
<p><strong>打包流程</strong></p> <p><strong>打包流程</strong></p>
<ol> <ol>
<li>选择要打包的已下载网站</li> <li>确保已安装 Go Wails3 开发环境</li>
<li>选择要打包的网站文件夹(包含HTMLCSSJS等静态文件)</li>
<li>填写应用的基本信息名称版本作者等</li> <li>填写应用的基本信息名称版本作者等</li>
<li>配置打包参数目标平台窗口大小</li> <li>配置打包参数目标平台窗口大小输出目录</li>
<li>确认信息无误后开始打包</li> <li>点击"开始打包"等待编译完成</li>
</ol> </ol>
<p><strong>打包原理</strong></p>
<ul>
<li>自动创建一个新的 Wails3 项目</li>
<li>将您的静态网站文件嵌入到应用中</li>
<li>编译生成独立的桌面应用程序(.exe)</li>
<li>应用内置 WebView无需浏览器即可运行</li>
</ul>
<p><strong>注意事项</strong></p> <p><strong>注意事项</strong></p>
<ul> <ul>
<li>打包过程可能需要几分钟请耐心等待</li> <li>首次打包需要下载依赖可能耗时5-10分钟</li>
<li>确保有足够的磁盘空间用于存储打包文件</li> <li>确保有足够的磁盘空间建议至少500MB</li>
<li>不同平台的打包文件格式不同Windows: .exe, macOS: .app, Linux: .AppImage</li> <li>网站文件夹应包含index.html作为入口</li>
<li>打包后的应用可独立运行无需安装额外环境</li>
<li>Windows平台生成.exe文件macOS生成.appLinux生成可执行文件</li>
</ul>
<p><strong>输出位置</strong></p>
<ul>
<li>默认输出到项目的 <code>site-dist</code> 目录</li>
<li>可自定义输出目录</li>
<li>打包完成后会在输出目录中找到可执行文件</li>
</ul> </ul>
</div> </div>
</a-card> </a-card>
@@ -1232,6 +1281,19 @@ const goToDownload = () => {
margin: 8px 0; margin: 8px 0;
} }
.help-content code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 4px;
font-family: 'Consolas', monospace;
font-size: 13px;
color: #d63384;
}
.help-content p {
margin: 12px 0;
}
/* 通用样式 */ /* 通用样式 */
.full-width { .full-width {
width: 100%; width: 100%;

340
services/pack_service.go Normal file
View File

@@ -0,0 +1,340 @@
package services
import (
"fmt"
"go-site-clone/utils"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
)
// PackService 打包服务
type PackService struct{}
// PackConfig 打包配置
type PackConfig struct {
SitePath string // 网站文件夹路径
AppName string // 应用名称
Version string // 版本号
Author string // 作者
Description string // 描述
Platforms []string // 目标平台
Width int // 窗口宽度
Height int // 窗口高度
OutputDir string // 输出目录
}
// Wails项目模板
const mainGoTemplate = `package main
import (
"embed"
"log"
"github.com/wailsapp/wails/v3/pkg/application"
)
//go:embed all:frontend
var assets embed.FS
func main() {
app := application.New(application.Options{
Name: "{{.AppName}}",
Description: "{{.Description}}",
Assets: application.AssetOptions{
Handler: application.AssetFileServerFS(assets),
},
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
})
app.Window.NewWithOptions(application.WebviewWindowOptions{
Title: "{{.AppName}}",
Mac: application.MacWindow{
InvisibleTitleBarHeight: 50,
Backdrop: application.MacBackdropTranslucent,
TitleBar: application.MacTitleBarHiddenInset,
},
BackgroundColour: application.NewRGB(255, 255, 255),
URL: "/",
Width: {{.Width}},
Height: {{.Height}},
})
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
`
const goModTemplate = `module {{.ModuleName}}
go 1.21
require github.com/wailsapp/wails/v3 v3.0.0-alpha.27
`
// CreateWailsProject 创建Wails项目结构
func (p *PackService) CreateWailsProject(config PackConfig, projectDir string) error {
// 1. 创建项目目录结构
dirs := []string{
projectDir,
filepath.Join(projectDir, "frontend"),
filepath.Join(projectDir, "build"),
}
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("创建目录失败 %s: %v", dir, err)
}
}
// 1.1 复制项目根目录的 Taskfile.yml
currentDir, err := os.Getwd()
if err != nil {
return fmt.Errorf("获取当前目录失败: %v", err)
}
srcTaskfile := filepath.Join(currentDir, "Taskfile.yml")
dstTaskfile := filepath.Join(projectDir, "Taskfile.yml")
if err := p.copyFile(srcTaskfile, dstTaskfile); err != nil {
return fmt.Errorf("复制Taskfile.yml失败: %v", err)
}
// 1.2 复制整个 build 目录
srcBuildDir := filepath.Join(currentDir, "build")
dstBuildDir := filepath.Join(projectDir, "build")
if err := p.copyDirectory(srcBuildDir, dstBuildDir); err != nil {
return fmt.Errorf("复制build目录失败: %v", err)
}
// 2. 复制网站文件到frontend目录
frontendDir := filepath.Join(projectDir, "frontend")
if err := p.copyDirectory(config.SitePath, frontendDir); err != nil {
return fmt.Errorf("复制网站文件失败: %v", err)
}
// 2.1 创建一个空的 package.json避免构建系统报错
packageJSON := `{
"name": "static-site",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "echo 'Static site, no build needed'",
"generate": "echo 'Static site, no build needed'"
}
}`
packageJSONPath := filepath.Join(frontendDir, "package.json")
if err := os.WriteFile(packageJSONPath, []byte(packageJSON), 0644); err != nil {
return fmt.Errorf("创建package.json失败: %v", err)
}
// 2.2 创建一个空的 dist 目录(如果构建系统需要)
distDir := filepath.Join(frontendDir, "dist")
if err := os.MkdirAll(distDir, 0755); err != nil {
return fmt.Errorf("创建dist目录失败: %v", err)
}
// 3. 生成main.go
mainGoPath := filepath.Join(projectDir, "main.go")
tmpl, err := template.New("main").Parse(mainGoTemplate)
if err != nil {
return fmt.Errorf("解析main.go模板失败: %v", err)
}
mainFile, err := os.Create(mainGoPath)
if err != nil {
return fmt.Errorf("创建main.go失败: %v", err)
}
defer mainFile.Close()
templateData := map[string]interface{}{
"AppName": config.AppName,
"Description": config.Description,
"Width": config.Width,
"Height": config.Height,
}
if err := tmpl.Execute(mainFile, templateData); err != nil {
return fmt.Errorf("生成main.go失败: %v", err)
}
// 4. 生成go.mod
goModPath := filepath.Join(projectDir, "go.mod")
modTmpl, err := template.New("gomod").Parse(goModTemplate)
if err != nil {
return fmt.Errorf("解析go.mod模板失败: %v", err)
}
modFile, err := os.Create(goModPath)
if err != nil {
return fmt.Errorf("创建go.mod失败: %v", err)
}
defer modFile.Close()
modData := map[string]interface{}{
"ModuleName": strings.ToLower(strings.ReplaceAll(config.AppName, " ", "-")),
}
if err := modTmpl.Execute(modFile, modData); err != nil {
return fmt.Errorf("生成go.mod失败: %v", err)
}
return nil
}
// BuildWailsApp 构建Wails应用
func (p *PackService) BuildWailsApp(projectDir string, outputDir string, platforms []string) error {
// 获取环境状态
envStatus, err := utils.GetEnvStatus()
if err != nil {
return fmt.Errorf("检查环境失败: %v", err)
}
// 确定使用的Go命令
goCmd := "go"
if envStatus.GoPath != "" {
goCmd = envStatus.GoPath
}
// 1. 执行 go mod tidy
tidyCmd := exec.Command(goCmd, "mod", "tidy")
tidyCmd.Dir = projectDir
if output, err := tidyCmd.CombinedOutput(); err != nil {
return fmt.Errorf("go mod tidy 失败: %v\n%s", err, output)
}
// 2. 直接使用 go build 编译,跳过复杂的构建流程
// 创建输出目录
if err := os.MkdirAll(outputDir, 0755); err != nil {
return fmt.Errorf("创建输出目录失败: %v", err)
}
// 确定输出文件名
appName := filepath.Base(projectDir)
outputFile := filepath.Join(outputDir, appName+".exe")
// 构建命令,使用生产环境标签
buildCmd := exec.Command(goCmd, "build",
"-tags", "production",
"-ldflags", "-w -s -H windowsgui",
"-o", outputFile,
".")
buildCmd.Dir = projectDir
// 设置环境变量
buildCmd.Env = append(os.Environ(),
"CGO_ENABLED=1",
"GOOS=windows",
)
output, err := buildCmd.CombinedOutput()
if err != nil {
return fmt.Errorf("go build 失败: %v\n%s", err, output)
}
return nil
}
// copyDirectory 递归复制目录
func (p *PackService) copyDirectory(src, dst string) error {
// 获取源目录信息
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
// 创建目标目录
if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
return err
}
// 读取源目录
entries, err := os.ReadDir(src)
if err != nil {
return err
}
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
if entry.IsDir() {
// 递归复制子目录
if err := p.copyDirectory(srcPath, dstPath); err != nil {
return err
}
} else {
// 复制文件
if err := p.copyFile(srcPath, dstPath); err != nil {
return err
}
}
}
return nil
}
// copyFile 复制单个文件
func (p *PackService) copyFile(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
// 复制文件权限
srcInfo, err := os.Stat(src)
if err != nil {
return err
}
return os.Chmod(dst, srcInfo.Mode())
}
// copyBuildOutput 复制构建输出
func (p *PackService) copyBuildOutput(buildDir, outputDir string) error {
if err := os.MkdirAll(outputDir, 0755); err != nil {
return err
}
// 检查 buildDir 是否存在
if _, err := os.Stat(buildDir); os.IsNotExist(err) {
return fmt.Errorf("构建目录不存在: %s", buildDir)
}
entries, err := os.ReadDir(buildDir)
if err != nil {
return err
}
for _, entry := range entries {
srcPath := filepath.Join(buildDir, entry.Name())
dstPath := filepath.Join(outputDir, entry.Name())
if entry.IsDir() {
if err := p.copyDirectory(srcPath, dstPath); err != nil {
return err
}
} else {
if err := p.copyFile(srcPath, dstPath); err != nil {
return err
}
}
}
return nil
}