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