first commit

This commit is contained in:
dong 2025-05-13 17:10:30 +08:00
commit b4e777b707
6 changed files with 341 additions and 0 deletions

84
README.md Normal file
View File

@ -0,0 +1,84 @@
# 自解压安装程序
## 简介
这是一个自解压安装程序工具,可以将多个文件打包成一个单独的可执行文件(.exe使用户能够通过运行该可执行文件来自动解压并安装所需文件。该工具特别适合于Windows环境下的软件分发和安装。
## 功能特点
- **自包含**:将安装所需的所有文件打包到一个单独的可执行文件中
- **自动解压**:运行时自动将文件解压到临时目录
- **自动安装**解压后自动运行安装脚本install.bat
- **隐藏控制台**:运行过程中隐藏命令行窗口,提供更好的用户体验
- **自定义图标**支持使用自定义图标app.ico美化安装程序
## 使用方法
### 创建自解压安装程序
1. 准备以下文件:
- `install.bat`:安装脚本,将在解压后自动执行
- `mswsock.dll`需要打包的DLL文件
- `app.ico`:(可选)自定义图标文件
2. 运行程序:
```
build.exe
```
3. 程序将生成 `install.exe` 自解压安装文件
### 使用生成的安装程序
用户只需双击生成的 `install.exe` 文件,程序将:
1. 自动检测是否为自解压模式
2. 将打包的文件解压到临时目录(`%TEMP%\myapp_install`
3. 自动运行解压后的 `install.bat` 脚本完成安装
## 技术原理
该程序使用以下技术实现自解压功能:
1. **自检测机制**:通过检查文件末尾的特殊标记来判断是否为自解压模式
2. **文件打包**使用Go语言的zip库将文件打包成zip格式
3. **数据附加**将zip数据附加到可执行文件末尾并添加8字节的大小标记
4. **解压机制**运行时从自身读取并解压zip数据到临时目录
5. **静默安装**使用Windows API隐藏控制台窗口提供无干扰的安装体验
## 开发说明
### 程序结构
- `main()`:主函数,判断运行模式并调用相应功能
- `isSelfExtracting()`:检测是否为自解压模式
- `createSelfExtractingExe()`:创建自解压可执行文件
- `extractFiles()`:从自身提取文件到临时目录
- `runInstallBat()`:运行安装脚本
- `addFileToZip()`将文件添加到zip包
- `hideConsoleWindow()`:隐藏控制台窗口
### 自定义
- 修改 `requiredFiles` 数组可以更改需要打包的文件列表
- 更换 `app.ico` 文件可以自定义安装程序图标
- 修改 `install.bat` 可以自定义安装过程
## 注意事项
1. 确保所有需要打包的文件都存在于程序运行目录
2. 如果需要管理员权限运行安装脚本,请在 `install.bat` 中添加相应代码
3. 生成的安装程序可能会被某些杀毒软件误报,这是因为自解压程序的特性与某些恶意软件类似
## 系统要求
- 操作系统Windows
- 无其他特殊依赖
## 构建方式
- 生成环境go mod init build
- go generate
- 生成图标rsrc -ico app.ico -o app.syso
- 构建32位set GOARCH=386
- 构建程序go build -ldflags="-H windowsgui" -o builder.exe

BIN
app.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
app.syso Normal file

Binary file not shown.

BIN
builder.exe Normal file

Binary file not shown.

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module builder
go 1.20

254
main.go Normal file
View File

@ -0,0 +1,254 @@
package main
import (
"archive/zip"
"bytes"
"encoding/binary"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"syscall"
)
//go:generate go run github.com/akavel/rsrc -ico app.ico -o app.syso
func main() {
hideConsoleWindow()
if isSelfExtracting() {
extractFiles()
return
}
createSelfExtractingExe()
}
// 检查自身是否是自解压文件
func isSelfExtracting() bool {
exePath, err := os.Executable()
if err != nil {
return false
}
exeFile, err := os.Open(exePath)
if err != nil {
return false
}
defer exeFile.Close()
fileInfo, err := exeFile.Stat()
if err != nil {
return false
}
fileSize := fileInfo.Size()
// 检查文件末尾是否有zip大小标记
if _, err = exeFile.Seek(-8, io.SeekEnd); err != nil {
return false
}
var zipSize uint64
if err := binary.Read(exeFile, binary.LittleEndian, &zipSize); err != nil {
return false
}
// 验证zip大小是否合理
if zipSize == 0 || zipSize > uint64(fileSize)-8 {
return false
}
return true
}
func hideConsoleWindow() {
kernel32 := syscall.NewLazyDLL("kernel32.dll")
proc := kernel32.NewProc("GetConsoleWindow")
hwnd, _, _ := proc.Call()
if hwnd != 0 {
user32 := syscall.NewLazyDLL("user32.dll")
showWindow := user32.NewProc("ShowWindow")
showWindow.Call(hwnd, 0) // SW_HIDE
}
}
func createSelfExtractingExe() {
outputExe := "install.exe"
requiredFiles := []string{"install.bat", "mswsock.dll"}
// 检查图标文件是否存在
if _, err := os.Stat("app.ico"); os.IsNotExist(err) {
log.Println("警告: app.ico 图标文件不存在,将使用默认图标")
}
for _, file := range requiredFiles {
if _, err := os.Stat(file); os.IsNotExist(err) {
log.Fatalf("文件 %s 不存在", file)
}
}
zipBuffer := new(bytes.Buffer)
zipWriter := zip.NewWriter(zipBuffer)
for _, file := range requiredFiles {
if err := addFileToZip(zipWriter, file); err != nil {
log.Fatalf("添加文件到 zip 失败: %v", err)
}
}
if err := zipWriter.Close(); err != nil {
log.Fatalf("关闭 zip writer 失败: %v", err)
}
outFile, err := os.Create(outputExe)
if err != nil {
log.Fatalf("创建输出文件失败: %v", err)
}
defer outFile.Close()
stub, err := os.ReadFile(os.Args[0])
if err != nil {
log.Fatalf("读取 stub 程序失败: %v", err)
}
if _, err = outFile.Write(stub); err != nil {
log.Fatalf("写入 stub 程序失败: %v", err)
}
if _, err = outFile.Write(zipBuffer.Bytes()); err != nil {
log.Fatalf("写入 zip 数据失败: %v", err)
}
zipSize := uint64(zipBuffer.Len())
if err := binary.Write(outFile, binary.LittleEndian, zipSize); err != nil {
log.Fatalf("写入 zip 大小标记失败: %v", err)
}
log.Printf("成功创建自解压文件: %s", outputExe)
}
func extractFiles() {
exePath, err := os.Executable()
if err != nil {
log.Fatalf("获取可执行文件路径失败: %v", err)
}
exeFile, err := os.Open(exePath)
if err != nil {
log.Fatalf("打开可执行文件失败: %v", err)
}
defer exeFile.Close()
fileInfo, err := exeFile.Stat()
if err != nil {
log.Fatalf("获取文件信息失败: %v", err)
}
fileSize := fileInfo.Size()
if _, err = exeFile.Seek(-8, io.SeekEnd); err != nil {
log.Fatalf("定位 zip 大小标记失败: %v", err)
}
var zipSize uint64
if err := binary.Read(exeFile, binary.LittleEndian, &zipSize); err != nil {
log.Fatalf("读取 zip 大小失败: %v", err)
}
zipOffset := fileSize - 8 - int64(zipSize)
if _, err = exeFile.Seek(zipOffset, io.SeekStart); err != nil {
log.Fatalf("定位 zip 数据失败: %v", err)
}
zipData := make([]byte, zipSize)
if _, err = io.ReadFull(exeFile, zipData); err != nil {
log.Fatalf("读取 zip 数据失败: %v", err)
}
zipReader, err := zip.NewReader(bytes.NewReader(zipData), int64(len(zipData)))
if err != nil {
log.Fatalf("创建 zip reader 失败: %v", err)
}
// 将文件解压到临时目录
tempDir := os.TempDir()
extractDir := filepath.Join(tempDir, "myapp_install")
if err := os.MkdirAll(extractDir, 0755); err != nil {
log.Fatalf("创建解压目录失败: %v", err)
}
log.Printf("正在解压文件到临时目录: %s", extractDir)
for _, file := range zipReader.File {
if file.FileInfo().IsDir() {
continue
}
rc, err := file.Open()
if err != nil {
log.Fatalf("打开 zip 中的文件 %s 失败: %v", file.Name, err)
}
targetPath := filepath.Join(extractDir, filepath.Base(file.Name))
outFile, err := os.Create(targetPath)
if err != nil {
rc.Close()
log.Fatalf("创建目标文件 %s 失败: %v", targetPath, err)
}
_, err = io.Copy(outFile, rc)
outFile.Close()
rc.Close()
if err != nil {
log.Fatalf("写入文件 %s 失败: %v", targetPath, err)
}
log.Printf("已解压文件: %s", targetPath)
}
// 运行install.bat无闪现窗口
runInstallBat(extractDir)
}
func runInstallBat(extractDir string) {
installBat := filepath.Join(extractDir, "install.bat")
// 方法1直接创建新窗口运行不需要管理员权限时
cmd := exec.Command("cmd", "/c", "start", "cmd", "/c", installBat)
cmd.Dir = extractDir
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
if err := cmd.Start(); err != nil {
log.Printf("启动install.bat失败: %v", err)
}
}
func addFileToZip(zipWriter *zip.Writer, filename string) error {
fileToZip, err := os.Open(filename)
if err != nil {
return err
}
defer fileToZip.Close()
info, err := fileToZip.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = filepath.Base(filename)
header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(writer, fileToZip)
return err
}