commit b4e777b707609bd1b223fc9371d6e49b428e721d Author: dong <1278815766@qq.com> Date: Tue May 13 17:10:30 2025 +0800 first commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..4047c6d --- /dev/null +++ b/README.md @@ -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 \ No newline at end of file diff --git a/app.ico b/app.ico new file mode 100644 index 0000000..1bfcd23 Binary files /dev/null and b/app.ico differ diff --git a/app.syso b/app.syso new file mode 100644 index 0000000..a2828c1 Binary files /dev/null and b/app.syso differ diff --git a/builder.exe b/builder.exe new file mode 100644 index 0000000..8274164 Binary files /dev/null and b/builder.exe differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f266fc5 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module builder + +go 1.20 diff --git a/main.go b/main.go new file mode 100644 index 0000000..b837f92 --- /dev/null +++ b/main.go @@ -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 +}