train-osource/pkg/installer/installer.go
2025-02-05 17:25:52 +03:00

278 lines
7.3 KiB
Go

package installer
import (
"archive/tar"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"train/pkg/manifest"
"train/pkg/paths"
)
var (
binInstallDir = filepath.Join(paths.BaseDir(), "installed_bins")
buildInstallDir = filepath.Join(paths.BaseDir(), "installed_packages")
)
func logInfo(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fmt.Printf("\033[32m==>\033[0m %s\n", msg)
}
func logTrain(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
fmt.Printf("\U0001F682 %s\n", msg)
}
func computeChecksum(filePath string) (string, error) {
data, err := ioutil.ReadFile(filePath)
if err != nil {
return "", err
}
hash := sha256.Sum256(data)
return hex.EncodeToString(hash[:]), nil
}
func downloadFile(url string) (string, error) {
logInfo("Downloading %s", url)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
tmpFile, err := ioutil.TempFile("", "package_")
if err != nil {
return "", err
}
_, err = io.Copy(tmpFile, resp.Body)
if err != nil {
tmpFile.Close()
return "", err
}
tmpFile.Close()
logInfo("File downloaded to temporary location")
return tmpFile.Name(), nil
}
func unpackPackage(filePath, packageName string) error {
logInfo("Unpacking package into %s", filepath.Join(buildInstallDir, packageName))
destDir := filepath.Join(buildInstallDir, packageName)
if err := os.MkdirAll(destDir, os.ModePerm); err != nil {
return err
}
f, err := os.Open(filePath)
if err != nil {
return err
}
defer f.Close()
gzr, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gzr.Close()
tr := tar.NewReader(gzr)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
target := filepath.Join(destDir, header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
return err
}
case tar.TypeReg:
outFile, err := os.Create(target)
if err != nil {
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return err
}
outFile.Close()
}
}
logInfo("Unpacking completed")
return nil
}
func executeBuildCommands(commands []string, packageName string) error {
packagePath := filepath.Join(buildInstallDir, packageName)
for _, commandStr := range commands {
logInfo("Executing build command: %s", commandStr)
cmd := exec.Command("sh", "-c", commandStr)
cmd.Dir = packagePath
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("command '%s' failed: %s", commandStr, string(output))
}
}
return nil
}
func saveInstalledPackage(name string, m *manifest.Manifest) error {
logInfo("Saving manifest to %s", filepath.Join(buildInstallDir, name))
packagePath := filepath.Join(buildInstallDir, name)
if err := os.MkdirAll(packagePath, os.ModePerm); err != nil {
return err
}
jsonData, err := json.MarshalIndent(m, "", " ")
if err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(packagePath, "manifest.json"), jsonData, 0644)
}
func InstallPackage(name string) error {
logTrain("Installing package: %s", name)
m, err := manifest.FetchManifest(name)
if err != nil {
return fmt.Errorf("failed to fetch manifest: %w", err)
}
if strings.ToLower(m.Mode) == "bin" {
logInfo("Installing in binary mode")
platformKey := runtime.GOOS + "_" + runtime.GOARCH
binaryInfo, ok := m.Binaries[platformKey]
if !ok {
return fmt.Errorf("no binary available for platform %s", platformKey)
}
packageFile, err := downloadFile(binaryInfo.URL)
if err != nil {
return fmt.Errorf("failed to download binary: %w", err)
}
defer os.Remove(packageFile)
computedChecksum, err := computeChecksum(packageFile)
if err != nil {
return fmt.Errorf("failed to compute checksum: %w", err)
}
if computedChecksum != binaryInfo.Checksum {
return fmt.Errorf("checksum mismatch: expected %s, got %s", binaryInfo.Checksum, computedChecksum)
}
if err := os.MkdirAll(binInstallDir, os.ModePerm); err != nil {
return err
}
destPath := filepath.Join(binInstallDir, m.Name)
data, err := ioutil.ReadFile(packageFile)
if err != nil {
return err
}
if err := ioutil.WriteFile(destPath, data, 0755); err != nil {
return err
}
logInfo("Binary package %s installed in %s", m.Name, destPath)
logTrain("Package %s successfully installed.", m.Name)
return nil
} else if strings.ToLower(m.Mode) == "build" {
logInfo("Installing in build mode")
packageFile, err := downloadFile(m.Source)
if err != nil {
return fmt.Errorf("failed to download package: %w", err)
}
defer os.Remove(packageFile)
computedChecksum, err := computeChecksum(packageFile)
if err != nil {
return fmt.Errorf("failed to compute checksum: %w", err)
}
if computedChecksum != m.Checksum {
return fmt.Errorf("checksum mismatch: expected %s, got %s", m.Checksum, computedChecksum)
}
if err := unpackPackage(packageFile, name); err != nil {
return fmt.Errorf("failed to unpack package: %w", err)
}
if len(m.Build) > 0 {
if err := executeBuildCommands(m.Build, name); err != nil {
return fmt.Errorf("build command error: %w", err)
}
}
if err := saveInstalledPackage(name, m); err != nil {
return fmt.Errorf("failed to save manifest: %w", err)
}
logTrain("Build package %s installed successfully.", m.Name)
return nil
}
return errors.New("unknown package mode")
}
func RemovePackage(name string) error {
logTrain("Removing package: %s", name)
packagePath := filepath.Join(buildInstallDir, name)
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
return errors.New("package not found")
}
manifestPath := filepath.Join(packagePath, "manifest.json")
data, err := ioutil.ReadFile(manifestPath)
if err == nil {
var m manifest.Manifest
if err := json.Unmarshal(data, &m); err == nil {
if len(m.Uninstall) > 0 {
for _, cmdStr := range m.Uninstall {
logInfo("Executing uninstall command: %s", cmdStr)
cmd := exec.Command("sh", "-c", cmdStr)
cmd.Dir = packagePath
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("uninstall command '%s' failed: %s", cmdStr, string(output))
}
}
}
}
}
logInfo("Removing directory %s", packagePath)
if err := os.RemoveAll(packagePath); err != nil {
return fmt.Errorf("failed to remove package: %w", err)
}
logTrain("Package %s removed.", name)
return nil
}
func UpdatePackage(name string) error {
logTrain("Updating package: %s", name)
if err := RemovePackage(name); err != nil {
return fmt.Errorf("failed to remove package for update: %w", err)
}
if err := InstallPackage(name); err != nil {
return fmt.Errorf("failed to install package: %w", err)
}
logTrain("Package %s updated successfully.", name)
return nil
}
func ListInstalledPackages() ([]string, error) {
var pkgs []string
entries, err := ioutil.ReadDir(buildInstallDir)
if err != nil {
return nil, err
}
for _, entry := range entries {
if entry.IsDir() {
pkgs = append(pkgs, entry.Name())
}
}
return pkgs, nil
}
func GetPackageInfo(name string) (string, error) {
infoFile := filepath.Join(buildInstallDir, name, "manifest.json")
data, err := ioutil.ReadFile(infoFile)
if err != nil {
return "", err
}
return string(data), nil
}