first commit
This commit is contained in:
commit
b4e777b707
84
README.md
Normal file
84
README.md
Normal 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
builder.exe
Normal file
BIN
builder.exe
Normal file
Binary file not shown.
254
main.go
Normal file
254
main.go
Normal 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
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user